diff --git a/.github/workflows/xbox-gdk-desktop.yml b/.github/workflows/xbox-gdk-desktop.yml index a16baa7faf..8bd61574f4 100644 --- a/.github/workflows/xbox-gdk-desktop.yml +++ b/.github/workflows/xbox-gdk-desktop.yml @@ -22,7 +22,7 @@ on: paths: - "xbox/native-bios/**" - "xbox/runtime/**" - - "xbox/live/hello.js" + - "xbox/live/oskiewar.js" - ".github/workflows/xbox-gdk-desktop.yml" pull_request: paths: diff --git a/apple/oskiewar/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/apple/oskiewar/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..4d007b988eb75ddae04540ddfdc122bb84655872 GIT binary patch literal 67860 zcmeAS@N?(olHy`uVBq!ia0y~yU||4Z4mJh`hI(1;W(EcZ#^NA%Cx&(BWL`2bFu0^f zc&7RKGH5X{FmNz1wr7GhFfuSONHKr_^8!W&W{@TZMh1ojOfXp%h6T(BHb~*+yRT#! z7#tWpT^vIy7~kAwuaG@@^~1--_g1Gyz7w5&uX_E$y_}pX54i<+7?~Ocgq%G5IdoZg z_I!zxxy8+?DqwI`A}8ix*#^UH2lskz(uv%A`u?2k4#TkPbNBAH{$D9)tMxsr{F!-m zdfNGS=Z@G~SDq77RBT{i;!r>$99FPCa*nm)YDW`iU=n0xVvylLQOv-^VWHr_aG=p^ zSqHj=0K)?b2L?tK0VLu8BO?<7Ux<;b3z|d&1H*9)3;8$%7z!3l(o{lqAp;W&g9RJ1 zu^__=92yuJn5X&(3L@DDl2BkcV2I>Duq_SDOe_pCD^j{Tz+5DPk)aRO$qbAvG6D(= z4;)m3T~PeOz{tYEP{4y6b|7;eG%zqSuxrg!QbM!TfuSK8yx=)QmBjs?l=HXaR!AEFjis0Ww;Ez$%*2Y7rI^kP3OU1%lSP z8Et_;5(cczHrm*Og#@JOJz9W_xB_GX2P3q_`NMg^+O46lUcFkfZkgMwth3*5-Fmw( z>+F4LX{jX}HwMl+6J8t~yxV^JmMtF9(X(?5d;bxS z|F!$ypQrbQgoRZtRb7M5ga;KB?Y3XOWSN&++Oj3-DK&4kV=sMu|MI0!{x)wXNb+mE z!7aehQ7b!v8B|_13UI$T`QzEAPbb2!KJ!|6XHVoR?d^5zjqJl`&i`7S`ZR5&x%%I@ zWgZF-x)U1C{#};3#M{qrUCQ%W>*vfl@m=O~@wA_j`_rF4^T?~Kd*a>9rvfsRkwsvG zvV(s1@u_RUoj41nfZ$NorK+kyabaptrO!^UGmoFGv_HRR&WHS)n~n9g#$Yohm7y*{6P z-EH3X@(!g9u2+9@vGGbOHMj9iJ$Sfn=KJXSclqyRF6`#^fHb0a=s7SP_^RCSNx6Z6 ziI3|=@%Po?YbIPh9e(20)zv9~Uzu)l^#5{pN!5}MoGP<9dDycO5^e-Y8s+TZkv0u# z>y?q5*{@Kubg2u(v=SW$h6TyeagLx!YT~uD?SAWQ>WTB;e)#uY(F;#`+}~}P+#_cl<~DQv#pLp&M~>zE>|T8gOdKtK z4Ga!1R!w071zaA-i%(VA;p>dle|e|eoV0Y}
F`db{fu$C-3=wXmBhmGaf$q9mo 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%0D*2^2J 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 Cd;`J& literal 0 HcmV?d00001 diff --git a/apple/oskiewar/Assets.xcassets/AppIcon.appiconset/Contents.json b/apple/oskiewar/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..0d535452c5 --- /dev/null +++ b/apple/oskiewar/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,6 @@ +{ + "images": [ + {"filename":"AppIcon.png","idiom":"universal","platform":"ios","size":"1024x1024"} + ], + "info":{"author":"xcode","version":1} +} diff --git a/apple/oskiewar/Assets.xcassets/Contents.json b/apple/oskiewar/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..f223229d3b --- /dev/null +++ b/apple/oskiewar/Assets.xcassets/Contents.json @@ -0,0 +1 @@ +{"info":{"author":"xcode","version":1}} diff --git a/apple/oskiewar/Assets.xcassets/LaunchBackground.colorset/Contents.json b/apple/oskiewar/Assets.xcassets/LaunchBackground.colorset/Contents.json new file mode 100644 index 0000000000..cc0b60515b --- /dev/null +++ b/apple/oskiewar/Assets.xcassets/LaunchBackground.colorset/Contents.json @@ -0,0 +1,4 @@ +{ + "colors":[{"color":{"color-space":"srgb","components":{"alpha":"1.000","blue":"0.110","green":"0.031","red":"0.027"}},"idiom":"universal"}], + "info":{"author":"xcode","version":1} +} diff --git a/apple/oskiewar/Info.plist b/apple/oskiewar/Info.plist new file mode 100644 index 0000000000..06b7feccee --- /dev/null +++ b/apple/oskiewar/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleIdentifier$(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleExecutable$(EXECUTABLE_NAME) + CFBundleName$(PRODUCT_NAME) + CFBundlePackageTypeAPPL + CFBundleDisplayNameoskiewar + CFBundleIconNameAppIcon + CFBundleShortVersionString1.0.0 + CFBundleVersion7 + UILaunchScreenUIColorNameLaunchBackground + UIRequiresFullScreen + UISupportedInterfaceOrientations + UIInterfaceOrientationPortrait + UISupportedInterfaceOrientations~ipad + UIInterfaceOrientationPortraitUIInterfaceOrientationPortraitUpsideDown + UIStatusBarHidden + UIViewControllerBasedStatusBarAppearance + NSLocalNetworkUsageDescriptionOskiewar can connect to nearby players and music tools. + UIAppFontsComicRelief-Regular.ttf + + diff --git a/apple/oskiewar/Oskiewar.entitlements b/apple/oskiewar/Oskiewar.entitlements new file mode 100644 index 0000000000..899d535fec --- /dev/null +++ b/apple/oskiewar/Oskiewar.entitlements @@ -0,0 +1,3 @@ + + + diff --git a/apple/oskiewar/Oskiewar.xcodeproj/project.pbxproj b/apple/oskiewar/Oskiewar.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..85035940ec --- /dev/null +++ b/apple/oskiewar/Oskiewar.xcodeproj/project.pbxproj @@ -0,0 +1,389 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 1B43BAF5E41E93E3D9208197 /* product-analytics.mjs in Resources */ = {isa = PBXBuildFile; fileRef = A5CDAFE515D4323680797051 /* product-analytics.mjs */; }; + 21F3367D32F88CB3B2A52859 /* oskiewar-sfx.mjs in Resources */ = {isa = PBXBuildFile; fileRef = 2E725A92F239CBD5114A9A1E /* oskiewar-sfx.mjs */; }; + 359F92BBF72AEE001B8BFD71 /* oskiewar-analytics.mjs in Resources */ = {isa = PBXBuildFile; fileRef = A0AEFE9FB2DD8B296D83443D /* oskiewar-analytics.mjs */; }; + 4D00E7B7BFD0CDAA972C6F12 /* ComicRelief-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 179EE75D06881E64B8C2C164 /* ComicRelief-Regular.ttf */; }; + 5893AAD738853ADCCD1C6259 /* round-room.mjs in Resources */ = {isa = PBXBuildFile; fileRef = D5D135855C9371BD15F15377 /* round-room.mjs */; }; + 6F079803CD79934E3D119B92 /* mac-test.html in Resources */ = {isa = PBXBuildFile; fileRef = 8265144AA3E23B010A51E65A /* mac-test.html */; }; + 7729C117BEC5343C9CB8BAFC /* oskiewar-midi.mjs in Resources */ = {isa = PBXBuildFile; fileRef = 26C7F4ADC37CB30558AAE696 /* oskiewar-midi.mjs */; }; + 7D438ADCB6BECAD5D0E6C8F7 /* MetalSceneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 812FA65365747604E58B98E7 /* MetalSceneView.swift */; }; + 82A4DA7DFB26C3E87F9AB609 /* oskiewar.js in Resources */ = {isa = PBXBuildFile; fileRef = AB2FCD0A93F17F907A54302C /* oskiewar.js */; }; + 98991A7AC85B7CAE426E8D15 /* qr.mjs in Resources */ = {isa = PBXBuildFile; fileRef = A50E50A82D3F715FB8B31490 /* qr.mjs */; }; + C0C46FBB6805F1A1E055FED7 /* OskiewarApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43902C050551AD2D6DA0EF9 /* OskiewarApp.swift */; }; + DE990881EAE44FB2BE4592FF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FCECE2875B24DB7356CB0FDC /* Assets.xcassets */; }; + F10250364FF369A809057B31 /* frame-driver.mjs in Resources */ = {isa = PBXBuildFile; fileRef = 4C7C4D62E6C706C3AB30BB3B /* frame-driver.mjs */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 179EE75D06881E64B8C2C164 /* ComicRelief-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ComicRelief-Regular.ttf"; sourceTree = ""; }; + 26C7F4ADC37CB30558AAE696 /* oskiewar-midi.mjs */ = {isa = PBXFileReference; path = "oskiewar-midi.mjs"; sourceTree = ""; }; + 2E725A92F239CBD5114A9A1E /* oskiewar-sfx.mjs */ = {isa = PBXFileReference; path = "oskiewar-sfx.mjs"; sourceTree = ""; }; + 4C7C4D62E6C706C3AB30BB3B /* frame-driver.mjs */ = {isa = PBXFileReference; path = "frame-driver.mjs"; sourceTree = ""; }; + 812FA65365747604E58B98E7 /* MetalSceneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalSceneView.swift; sourceTree = ""; }; + 8265144AA3E23B010A51E65A /* mac-test.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "mac-test.html"; sourceTree = ""; }; + A0AEFE9FB2DD8B296D83443D /* oskiewar-analytics.mjs */ = {isa = PBXFileReference; path = "oskiewar-analytics.mjs"; sourceTree = ""; }; + A50E50A82D3F715FB8B31490 /* qr.mjs */ = {isa = PBXFileReference; path = qr.mjs; sourceTree = ""; }; + A5CDAFE515D4323680797051 /* product-analytics.mjs */ = {isa = PBXFileReference; path = "product-analytics.mjs"; sourceTree = ""; }; + AB2FCD0A93F17F907A54302C /* oskiewar.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = oskiewar.js; sourceTree = ""; }; + C43902C050551AD2D6DA0EF9 /* OskiewarApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OskiewarApp.swift; sourceTree = ""; }; + D5D135855C9371BD15F15377 /* round-room.mjs */ = {isa = PBXFileReference; path = "round-room.mjs"; sourceTree = ""; }; + F05D439B6814C2DE2C8CCBF7 /* Oskiewar.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Oskiewar.app; sourceTree = BUILT_PRODUCTS_DIR; }; + FCECE2875B24DB7356CB0FDC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 41977401E108AFD8508FEB8B /* Sources */ = { + isa = PBXGroup; + children = ( + 812FA65365747604E58B98E7 /* MetalSceneView.swift */, + C43902C050551AD2D6DA0EF9 /* OskiewarApp.swift */, + ); + path = Sources; + sourceTree = ""; + }; + 5B8907180072A0F4F1552C04 /* fonts */ = { + isa = PBXGroup; + children = ( + 179EE75D06881E64B8C2C164 /* ComicRelief-Regular.ttf */, + ); + name = fonts; + path = ../../system/public/papers.aesthetic.computer/foundry/fonts; + sourceTree = ""; + }; + 9CBE8B5C340FA47D25810A30 /* lib */ = { + isa = PBXGroup; + children = ( + A0AEFE9FB2DD8B296D83443D /* oskiewar-analytics.mjs */, + A5CDAFE515D4323680797051 /* product-analytics.mjs */, + ); + name = lib; + path = ../../system/public/aesthetic.computer/lib; + sourceTree = ""; + }; + A38CD8BDA4C5FDCC8523D207 /* qr */ = { + isa = PBXGroup; + children = ( + A50E50A82D3F715FB8B31490 /* qr.mjs */, + ); + name = qr; + path = "../../system/public/aesthetic.computer/dep/@akamfoad/qr"; + sourceTree = ""; + }; + B86C25C7192D4BF489863DD3 = { + isa = PBXGroup; + children = ( + FCECE2875B24DB7356CB0FDC /* Assets.xcassets */, + 5B8907180072A0F4F1552C04 /* fonts */, + 9CBE8B5C340FA47D25810A30 /* lib */, + E0FA0D9930BC45B45B9D078E /* live */, + A38CD8BDA4C5FDCC8523D207 /* qr */, + 41977401E108AFD8508FEB8B /* Sources */, + ED4D4D26449D520C8D063F1F /* Products */, + ); + sourceTree = ""; + }; + E0FA0D9930BC45B45B9D078E /* live */ = { + isa = PBXGroup; + children = ( + 4C7C4D62E6C706C3AB30BB3B /* frame-driver.mjs */, + 8265144AA3E23B010A51E65A /* mac-test.html */, + 26C7F4ADC37CB30558AAE696 /* oskiewar-midi.mjs */, + 2E725A92F239CBD5114A9A1E /* oskiewar-sfx.mjs */, + AB2FCD0A93F17F907A54302C /* oskiewar.js */, + D5D135855C9371BD15F15377 /* round-room.mjs */, + ); + name = live; + path = ../../xbox/live; + sourceTree = ""; + }; + ED4D4D26449D520C8D063F1F /* Products */ = { + isa = PBXGroup; + children = ( + F05D439B6814C2DE2C8CCBF7 /* Oskiewar.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D6286D05C2E5100975A6DFD3 /* Oskiewar */ = { + isa = PBXNativeTarget; + buildConfigurationList = AC9AF44B32267614B0997002 /* Build configuration list for PBXNativeTarget "Oskiewar" */; + buildPhases = ( + CBE857A579B09F47F7616678 /* Sources */, + 9FD85112367974CDC28A85B9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Oskiewar; + packageProductDependencies = ( + ); + productName = Oskiewar; + productReference = F05D439B6814C2DE2C8CCBF7 /* Oskiewar.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8D001550F21155AC339C20BF /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + D6286D05C2E5100975A6DFD3 = { + DevelopmentTeam = FB5948YR3S; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 742DBA68B21F7933A0C07CD1 /* Build configuration list for PBXProject "Oskiewar" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = B86C25C7192D4BF489863DD3; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = ED4D4D26449D520C8D063F1F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D6286D05C2E5100975A6DFD3 /* Oskiewar */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 9FD85112367974CDC28A85B9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE990881EAE44FB2BE4592FF /* Assets.xcassets in Resources */, + 4D00E7B7BFD0CDAA972C6F12 /* ComicRelief-Regular.ttf in Resources */, + F10250364FF369A809057B31 /* frame-driver.mjs in Resources */, + 6F079803CD79934E3D119B92 /* mac-test.html in Resources */, + 359F92BBF72AEE001B8BFD71 /* oskiewar-analytics.mjs in Resources */, + 7729C117BEC5343C9CB8BAFC /* oskiewar-midi.mjs in Resources */, + 21F3367D32F88CB3B2A52859 /* oskiewar-sfx.mjs in Resources */, + 82A4DA7DFB26C3E87F9AB609 /* oskiewar.js in Resources */, + 1B43BAF5E41E93E3D9208197 /* product-analytics.mjs in Resources */, + 98991A7AC85B7CAE426E8D15 /* qr.mjs in Resources */, + 5893AAD738853ADCCD1C6259 /* round-room.mjs in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CBE857A579B09F47F7616678 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7D438ADCB6BECAD5D0E6C8F7 /* MetalSceneView.swift in Sources */, + C0C46FBB6805F1A1E055FED7 /* OskiewarApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 59EC86057665E45491E5C218 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Oskiewar.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = computer.aesthetic.oskiewar; + PRODUCT_NAME = oskiewar; + SDKROOT = iphoneos; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6A016B6E7F9613AD1A8A8C5C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = FB5948YR3S; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 76F065EDF681572487AF5C0E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = FB5948YR3S; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + D6EE6F69FFC046A86BC8AC2E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Oskiewar.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = computer.aesthetic.oskiewar; + PRODUCT_NAME = oskiewar; + SDKROOT = iphoneos; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 742DBA68B21F7933A0C07CD1 /* Build configuration list for PBXProject "Oskiewar" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 76F065EDF681572487AF5C0E /* Debug */, + 6A016B6E7F9613AD1A8A8C5C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + AC9AF44B32267614B0997002 /* Build configuration list for PBXNativeTarget "Oskiewar" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 59EC86057665E45491E5C218 /* Debug */, + D6EE6F69FFC046A86BC8AC2E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8D001550F21155AC339C20BF /* Project object */; +} diff --git a/apple/oskiewar/Oskiewar.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apple/oskiewar/Oskiewar.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/apple/oskiewar/Oskiewar.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apple/oskiewar/Oskiewar.xcodeproj/xcshareddata/xcschemes/Oskiewar.xcscheme b/apple/oskiewar/Oskiewar.xcodeproj/xcshareddata/xcschemes/Oskiewar.xcscheme new file mode 100644 index 0000000000..7a18f09edb --- /dev/null +++ b/apple/oskiewar/Oskiewar.xcodeproj/xcshareddata/xcschemes/Oskiewar.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apple/oskiewar/Sources/MetalSceneView.swift b/apple/oskiewar/Sources/MetalSceneView.swift new file mode 100644 index 0000000000..173a726ca3 --- /dev/null +++ b/apple/oskiewar/Sources/MetalSceneView.swift @@ -0,0 +1,150 @@ +import MetalKit +import UIKit + +struct SceneVertex { + var position: SIMD4 + var color: SIMD4 +} + +/// Depth-bearing Oskiewar scene surface. The JavaScript camera already emits +/// projected logical x/y and canonical z; this view performs only the shared +/// logical-to-clip transform also used by the web and Xbox adapters. +final class MetalSceneView: MTKView, MTKViewDelegate { + static let maxTriangles = 8_192 + private var sceneVertices: [SceneVertex] = [] + private let commandQueue: MTLCommandQueue + private let pipeline: MTLRenderPipelineState + private let depthState: MTLDepthStencilState + private let vertexBuffer: MTLBuffer + private let sceneLock = NSLock() + private var background = MTLClearColor(red: 7 / 255, green: 8 / 255, + blue: 28 / 255, alpha: 1) + + init(frame: CGRect = .zero) { + guard let device = MTLCreateSystemDefaultDevice(), + let queue = device.makeCommandQueue() else { + fatalError("Metal is unavailable") + } + commandQueue = queue + guard let buffer = device.makeBuffer(length: Self.maxTriangles * 3 * + MemoryLayout.stride, options: .storageModeShared) else { + fatalError("Metal scene buffer allocation failed") + } + vertexBuffer = buffer + + let source = """ + #include + using namespace metal; + struct Vertex { float4 position; float4 color; }; + struct Raster { float4 position [[position]]; float3 color; }; + vertex Raster scene_vertex(const device Vertex *vertices [[buffer(0)]], + uint id [[vertex_id]]) { + Raster out; + out.position = vertices[id].position; + out.color = vertices[id].color.rgb; + return out; + } + fragment float4 scene_fragment(Raster in [[stage_in]]) { + return float4(in.color, 1.0); + } + """ + do { + let library = try device.makeLibrary(source: source, options: nil) + let descriptor = MTLRenderPipelineDescriptor() + descriptor.vertexFunction = library.makeFunction(name: "scene_vertex") + descriptor.fragmentFunction = library.makeFunction(name: "scene_fragment") + descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + descriptor.depthAttachmentPixelFormat = .depth32Float + pipeline = try device.makeRenderPipelineState(descriptor: descriptor) + } catch { + fatalError("Metal scene pipeline failed: \(error)") + } + let depth = MTLDepthStencilDescriptor() + depth.isDepthWriteEnabled = true + depth.depthCompareFunction = .lessEqual + guard let state = device.makeDepthStencilState(descriptor: depth) else { + fatalError("Metal depth state failed") + } + depthState = state + super.init(frame: frame, device: device) + colorPixelFormat = .bgra8Unorm + depthStencilPixelFormat = .depth32Float + clearDepth = 1 + framebufferOnly = true + isPaused = true + enableSetNeedsDisplay = true + preferredFramesPerSecond = 60 + delegate = self + isMultipleTouchEnabled = true + } + + required init(coder: NSCoder) { fatalError("init(coder:) is unsupported") } + + func beginFrame(red: Double, green: Double, blue: Double) { + sceneLock.lock() + sceneVertices.removeAll(keepingCapacity: true) + background = MTLClearColor(red: clamped(red), green: clamped(green), + blue: clamped(blue), alpha: 1) + sceneLock.unlock() + } + + @discardableResult + func triangle(_ values: [Double], logicalSize: CGSize) -> Bool { + guard values.count >= 12, logicalSize.width > 0, logicalSize.height > 0, + values.prefix(12).allSatisfy(\.isFinite) else { return false } + sceneLock.lock() + defer { sceneLock.unlock() } + guard sceneVertices.count < Self.maxTriangles * 3 else { return false } + let ink = SIMD4(Float(clamped255(values[9])), + Float(clamped255(values[10])), + Float(clamped255(values[11])), 1) + for vertex in 0..<3 { + let at = vertex * 3 + let x = Float(values[at] / Double(logicalSize.width / 2) - 1) + let y = Float(1 - values[at + 1] / Double(logicalSize.height / 2)) + let z = Float(max(0, min(1, (values[at + 2] + 1.5) / 3))) + sceneVertices.append(SceneVertex(position: SIMD4(x, y, z, 1), color: ink)) + } + return true + } + + func present() { setNeedsDisplay() } + + func draw(in view: MTKView) { + sceneLock.lock() + let vertices = sceneVertices + let clear = background + sceneLock.unlock() + guard let pass = currentRenderPassDescriptor, + let drawable = currentDrawable, + let command = commandQueue.makeCommandBuffer() else { return } + pass.colorAttachments[0].clearColor = clear + pass.colorAttachments[0].loadAction = .clear + pass.depthAttachment.clearDepth = 1 + pass.depthAttachment.loadAction = .clear + guard let encoder = command.makeRenderCommandEncoder(descriptor: pass) else { return } + if !vertices.isEmpty { + _ = vertices.withUnsafeBytes { bytes in + memcpy(vertexBuffer.contents(), bytes.baseAddress!, bytes.count) + } + encoder.setRenderPipelineState(pipeline) + encoder.setDepthStencilState(depthState) + encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + encoder.drawPrimitives(type: .triangle, vertexStart: 0, + vertexCount: vertices.count) + } + encoder.endEncoding() + command.present(drawable) + command.commit() + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} + + private func clamped(_ channel: Double) -> Double { + max(0, min(255, channel)) / 255 + } + + private func clamped255(_ channel: Double) -> Double { + max(0, min(255, channel)) / 255 + } +} diff --git a/apple/oskiewar/Sources/OskiewarApp.swift b/apple/oskiewar/Sources/OskiewarApp.swift new file mode 100644 index 0000000000..d52e57b065 --- /dev/null +++ b/apple/oskiewar/Sources/OskiewarApp.swift @@ -0,0 +1,149 @@ +import SwiftUI +import UIKit +import WebKit + +@main +struct OskiewarApp: App { + var body: some Scene { + WindowGroup { GameControllerSurface().ignoresSafeArea() } + } +} + +struct GameControllerSurface: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> GameSafetyController { + GameSafetyController(rootView: AnyView(GameSurface().ignoresSafeArea())) + } + + func updateUIViewController(_ controller: GameSafetyController, + context: Context) {} +} + +final class GameSafetyController: UIHostingController { + override var prefersHomeIndicatorAutoHidden: Bool { true } + override var prefersStatusBarHidden: Bool { true } + override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { .all } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + UIApplication.shared.isIdleTimerDisabled = true + setNeedsUpdateOfHomeIndicatorAutoHidden() + setNeedsUpdateOfScreenEdgesDeferringSystemGestures() + } + + override func viewWillDisappear(_ animated: Bool) { + UIApplication.shared.isIdleTimerDisabled = false + super.viewWillDisappear(animated) + } +} + +struct GameSurface: UIViewRepresentable { + func makeCoordinator() -> Coordinator { Coordinator() } + + func makeUIView(context: Context) -> WKWebView { + let configuration = WKWebViewConfiguration() + configuration.defaultWebpagePreferences.allowsContentJavaScript = true + configuration.allowsInlineMediaPlayback = true + configuration.mediaTypesRequiringUserActionForPlayback = [] + configuration.setURLSchemeHandler(context.coordinator.handler, + forURLScheme: "oskiewar") + let view = WKWebView(frame: .zero, configuration: configuration) + view.isMultipleTouchEnabled = true + view.isOpaque = false + view.backgroundColor = UIColor(red: 7/255, green: 8/255, blue: 28/255, alpha: 1) + view.scrollView.isScrollEnabled = false + view.scrollView.contentInsetAdjustmentBehavior = .never + view.navigationDelegate = context.coordinator + context.coordinator.webView = view + let debugTap = UITapGestureRecognizer(target: context.coordinator, + action: #selector(Coordinator.toggleDebug)) + debugTap.numberOfTouchesRequired = 5 + debugTap.numberOfTapsRequired = 1 + debugTap.cancelsTouchesInView = false + view.addGestureRecognizer(debugTap) + context.coordinator.loadProduction(in: view) + return view + } + + func updateUIView(_ view: WKWebView, context: Context) {} + + final class Coordinator: NSObject, WKNavigationDelegate { + let handler = BundleSchemeHandler() + weak var webView: WKWebView? + private var usingFallback = false + + func loadProduction(in webView: WKWebView) { + let url = URL(string: "https://oskiewar.com/?touch&app=ios")! + webView.load(URLRequest(url: url, + cachePolicy: .reloadIgnoringLocalCacheData)) + } + + private func loadFallback(in webView: WKWebView, error: Error) { + guard !usingFallback else { return } + usingFallback = true + print("oskiewar production unavailable; using bundled game: \(error)") + webView.load(URLRequest(url: URL(string: + "oskiewar://app/mac-test.html?touch&app=ios&offline")!)) + } + + @objc func toggleDebug() { + webView?.evaluateJavaScript("globalThis.__oskiewarToggleDebug?.()") + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + webView.evaluateJavaScript("document.documentElement.style.webkitUserSelect='none';document.documentElement.style.webkitTouchCallout='none';") + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, + withError error: Error) { loadFallback(in: webView, error: error) } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, + withError error: Error) { loadFallback(in: webView, error: error) } + } +} + +final class BundleSchemeHandler: NSObject, WKURLSchemeHandler { + private let routes: [String: (String, String)] = [ + "/": ("mac-test", "html"), + "/mac-test.html": ("mac-test", "html"), + "/oskiewar.js": ("oskiewar", "js"), + "/oskiewar-sfx.mjs": ("oskiewar-sfx", "mjs"), + "/oskiewar-midi.mjs": ("oskiewar-midi", "mjs"), + "/frame-driver.mjs": ("frame-driver", "mjs"), + "/round-room.mjs": ("round-room", "mjs"), + "/aesthetic.computer/dep/@akamfoad/qr/qr.mjs": ("qr", "mjs"), + "/aesthetic.computer/lib/product-analytics.mjs": ("product-analytics", "mjs"), + "/aesthetic.computer/lib/oskiewar-analytics.mjs": ("oskiewar-analytics", "mjs"), + "/ComicRelief-Regular.ttf": ("ComicRelief-Regular", "ttf"), + ] + + func webView(_ webView: WKWebView, start task: WKURLSchemeTask) { + guard let url = task.request.url, let resource = routes[url.path], + let file = Bundle.main.url(forResource: resource.0, withExtension: resource.1), + let data = try? Data(contentsOf: file) else { + respond(task, url: task.request.url, status: 404, + type: "text/plain", data: Data("not found".utf8)) + return + } + let type: String + switch resource.1 { + case "html": type = "text/html" + case "js", "mjs": type = "text/javascript" + case "ttf": type = "font/ttf" + default: type = "application/octet-stream" + } + respond(task, url: url, status: 200, type: type, data: data) + } + + func webView(_ webView: WKWebView, stop task: WKURLSchemeTask) {} + + private func respond(_ task: WKURLSchemeTask, url: URL?, status: Int, + type: String, data: Data) { + let response = HTTPURLResponse(url: url ?? URL(string: "oskiewar://app/")!, + statusCode: status, httpVersion: "HTTP/1.1", + headerFields: ["Content-Type": type, "Content-Length": "\(data.count)", + "Cache-Control": "no-store"])! + task.didReceive(response) + task.didReceive(data) + task.didFinish() + } +} diff --git a/apple/oskiewar/oskiewar-head-icon.svg b/apple/oskiewar/oskiewar-head-icon.svg new file mode 100644 index 0000000000..4bb7f93a00 --- /dev/null +++ b/apple/oskiewar/oskiewar-head-icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apple/oskiewar/project.yml b/apple/oskiewar/project.yml new file mode 100644 index 0000000000..25be20c513 --- /dev/null +++ b/apple/oskiewar/project.yml @@ -0,0 +1,49 @@ +name: Oskiewar +options: + bundleIdPrefix: computer.aesthetic + deploymentTarget: + iOS: "17.0" +settings: + base: + DEVELOPMENT_TEAM: FB5948YR3S + CODE_SIGN_STYLE: Automatic + SWIFT_VERSION: "5.0" +targets: + Oskiewar: + type: application + platform: iOS + sources: + - Sources + - path: ../../xbox/live/mac-test.html + buildPhase: resources + - path: ../../xbox/live/oskiewar.js + buildPhase: resources + - path: ../../xbox/live/oskiewar-sfx.mjs + buildPhase: resources + - path: ../../xbox/live/oskiewar-midi.mjs + buildPhase: resources + - path: ../../xbox/live/frame-driver.mjs + buildPhase: resources + - path: ../../xbox/live/round-room.mjs + buildPhase: resources + - path: ../../system/public/aesthetic.computer/dep/@akamfoad/qr/qr.mjs + buildPhase: resources + - path: ../../system/public/aesthetic.computer/lib/product-analytics.mjs + buildPhase: resources + - path: ../../system/public/aesthetic.computer/lib/oskiewar-analytics.mjs + buildPhase: resources + - path: ../../system/public/papers.aesthetic.computer/foundry/fonts/ComicRelief-Regular.ttf + buildPhase: resources + - path: Assets.xcassets + buildPhase: resources + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: computer.aesthetic.oskiewar + PRODUCT_NAME: oskiewar + INFOPLIST_FILE: Info.plist + ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon + TARGETED_DEVICE_FAMILY: "1,2" + SUPPORTS_MACCATALYST: NO + CODE_SIGN_ENTITLEMENTS: Oskiewar.entitlements + scheme: + gatherCoverageData: false diff --git a/oskiewar-world/ARCHITECTURE.md b/oskiewar-world/ARCHITECTURE.md index 8d892f5a3f..1ea4c20f7d 100644 --- a/oskiewar-world/ARCHITECTURE.md +++ b/oskiewar-world/ARCHITECTURE.md @@ -7,7 +7,7 @@ somewhere else entirely. Every platform-capability claim below is sourced, with the date it was read. Every performance number comes from a probe in `tmp/oskiewar-world/`, run on -this machine against `xbox/live/hello.js` at commit `2fcb1b6f4`. +this machine against `xbox/live/oskiewar.js` at commit `2fcb1b6f4`. --- @@ -21,8 +21,8 @@ client has to be corrected by interpolation — the thing the replay system already does with 1-second checkpoints, which is what you build when you can't trust re-simulation. -It can. The probe loads `hello.js` into a `new Function` sandbox with stub -hosts — the same trick `xbox/live/tests/hello.test.mjs` already uses — and +It can. The probe loads `oskiewar.js` into a `new Function` sandbox with stub +hosts — the same trick `xbox/live/tests/oskiewar.test.mjs` already uses — and drives `sim()` by advancing `runtime().monotonicUs` exactly 16,667 µs per call. No wall clock is consulted anywhere in the loop. @@ -54,9 +54,9 @@ match ids A: ow-tuzzu835 ow-thikke939 match ids B: ow-buvva169 ow-shammy583 ``` -At every round transition `pronounceableMatchName()` (`hello.js:487–498`) calls +At every round transition `pronounceableMatchName()` (`oskiewar.js:487–498`) calls `Math.random()` five times to build a round name. `seriesBallType()` -(`hello.js:1533`) then hashes that name to pick the match ball — and ball type +(`oskiewar.js:1533`) then hashes that name to pick the match ball — and ball type carries radius and mass. The comment above it reads "never the clock or Math.random," and it is wrong: the name it hashes is a `Math.random` product. One PRNG laundered through FNV-1a into the physics. @@ -104,7 +104,7 @@ seconds costs 31 ms. Reconciliation is affordable by a wide margin. The probe ran Node v24.18.1 on one machine. It does not prove bit-identical results between Node on the server and QuickJS on the console, or between x86 -and ARM. `hello.js` makes 84 transcendental calls (`Math.sin`, `Math.cos`, +and ARM. `oskiewar.js` makes 84 transcendental calls (`Math.sin`, `Math.cos`, `Math.atan2`, `**`), and those are the one place IEEE-754 does not guarantee agreement across implementations. QuickJS was not available on this machine to test against. @@ -604,7 +604,7 @@ different game. Today's camera rect-packs both fighters' hitboxes to fill the frame, and it took the action from 51% to 83% of action-safe height. It is the best change of the day. It is also unconditionally two-player: `fighterFrameRect()` -(`hello.js:1873`) packs a rect around the fighters and `cameraDoll.track()` +(`oskiewar.js:1873`) packs a rect around the fighters and `cameraDoll.track()` frames it. With eight players spread across 12,000 units, framing everyone means everyone @@ -701,9 +701,9 @@ console smurf costs an Xbox account too. ## 11. Where the sim runs -The probe already runs `hello.js` headless in Node, driven at a fixed tick, +The probe already runs `oskiewar.js` headless in Node, driven at a fixed tick, producing bit-identical results across processes. That is the interim answer -and it works today: the shard loads `hello.js` into a `new Function` sandbox +and it works today: the shard loads `oskiewar.js` into a `new Function` sandbox with stub hosts — `wipe`, `box`, `line`, `triangle`, `write` all no-ops — and never calls `paint()`. @@ -713,23 +713,23 @@ server process that needs none of it. **The destination is an extracted `oskiewar-sim.mjs`** — one module, imported by both the client and the shard, containing the physics, the hit resolution, the -round rules, and nothing that draws or makes sound. `hello.js` keeps the render +round rules, and nothing that draws or makes sound. `oskiewar.js` keeps the render pass, the audio, the input mapping, and the shell. The client imports the sim and calls it; the server imports the sim and calls it. One implementation, two callers, no drift possible by construction. -### What has to change in `hello.js` +### What has to change in `oskiewar.js` In dependency order, each independently verifiable: -1. **Seed the RNG.** `pronounceableMatchName()` (`hello.js:487–498`) is the +1. **Seed the RNG.** `pronounceableMatchName()` (`oskiewar.js:487–498`) is the sim's only nondeterminism, via `seriesBallType()` hashing its output into ball radius and mass. Seed it from the server-assigned match ID. Measured: fixes 2 of 5 diverging seeds; all 5 become bit-identical. *Verify:* `node tmp/oskiewar-world/wire.mjs 5400` reports `repeatIdentical: true` on every seed without the `seeded` override. -2. **Fixed timestep.** `gameSim()` (`hello.js:3388–3392`) derives dt from +2. **Fixed timestep.** `gameSim()` (`oskiewar.js:3388–3392`) derives dt from wall clock: `Math.min(0.04, Math.max(0.001, (now - lastSimAt) / 1000000))`. Replace with a fixed 1/60 and an accumulator that runs 0, 1, or 2 ticks per frame depending on elapsed real time. The client's render loop stays @@ -746,7 +746,7 @@ In dependency order, each independently verifiable: to iteration over a player list keyed by entity ID, with "the other player" becoming "the encounter opponent" — which is a real concept in the new design, not a workaround. - *Verify:* `grep -c 'players\[[01]\]' xbox/live/hello.js` reaches 0. + *Verify:* `grep -c 'players\[[01]\]' xbox/live/oskiewar.js` reaches 0. 5. **Widen the wire contracts.** `oskiewar-live-manager.mjs:69` and `oskiewar-replays.mjs:58` both reject any state where @@ -807,7 +807,7 @@ Each step is verifiable on its own and leaves the game playable. test suite still passes and that the probe's wall-clock test converges. 3. **Headless shard on WebSocket, 2 players, no prediction.** Node process, - `hello.js` in the sandbox, 60 Hz loop, 2-byte input up, JSON snapshot down + `oskiewar.js` in the sandbox, 60 Hz loop, 2-byte input up, JSON snapshot down at 20 Hz, client renders the server's state with no local sim. It will feel bad. Verify: two browsers fight, the server decides, nothing desyncs. diff --git a/package.json b/package.json index f2388a5e5b..02767f6d37 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,9 @@ "test:browser:local": "AC_TEST_URL=https://localhost:8888 node tests/browser/prompt-search.test.mjs", "xbox:test:mac": "sh xbox/tools/mac-test-app.sh", "xbox:play": "node xbox/tools/serve-live.mjs", + "oskiewar:deploy": "node xbox/tools/oskiewar-release.mjs deploy", + "oskiewar:parity": "node xbox/tools/oskiewar-release.mjs status", + "oskiewar:reconcile": "node xbox/tools/oskiewar-release.mjs reconcile", "xbox:test:oskiewar:blackbox": "node xbox/live/tests/blackbox-rounds.mjs", "xbox:burn:oskiewar-social": "node xbox/live/render-social-preview.mjs", "xbox:check:oskiewar-social": "node xbox/live/render-social-preview.mjs --check", diff --git a/papers/arxiv-oskiewar-reels/reels.tex b/papers/arxiv-oskiewar-reels/reels.tex index 6785e497d8..6a9d27798b 100644 --- a/papers/arxiv-oskiewar-reels/reels.tex +++ b/papers/arxiv-oskiewar-reels/reels.tex @@ -181,7 +181,7 @@ The code is the repository working tree at commit \code{daf4372f1} \cite{acrepo}: \code{xbox/live/marketing/} --- \code{source.mjs} (93 lines), \code{render.mjs} (367), \code{dress.mjs} (73), \code{publish.mjs} (209), \code{segments.mjs} (129), \code{shell.mjs} (135), \code{reel.mjs} (153) --- plus -\code{xbox/live/hello.js} (6,404 lines) and \code{xbox/live/mac-test.html} +\code{xbox/live/oskiewar.js} (6,404 lines) and \code{xbox/live/mac-test.html} (1,027). Claims about behaviour are grep-checked against that source. Where the operator's manual \cite{acmarketing} and the source disagree, the source wins and the divergence is stated. @@ -252,7 +252,7 @@ endpoint on 8 August 2026 it returned an authentication error, which means the fallback --- not the replay lane --- is what would run today. The seed works at all because of a property of the game rather than of the -factory. \code{hello.js} calls \code{Math.random} exactly once, at line 611, to +factory. \code{oskiewar.js} calls \code{Math.random} exactly once, at line 611, to seed the match-name generator; ball kind, round names, and the fighters all fall out of that one draw. Replacing \code{Math.random} with a seeded generator before the page boots therefore fixes the identity of the entire match from a @@ -262,7 +262,7 @@ earlier version drew ball radius and mass from \code{Math.random} directly, \section{Stage 2 --- Render: the Real Game, at the Real Shape} -Stage two runs \code{mac-test.html} and \code{hello.js} in headless Chrome +Stage two runs \code{mac-test.html} and \code{oskiewar.js} in headless Chrome through the same frame driver a player gets, served by a small local shell (\code{shell.mjs}) that lists every module the page imports. Video and audio are captured by two separate mechanisms, both borrowed from the sibling @@ -606,7 +606,7 @@ is enough. \subsection{A match is a round} -\code{hello.js} declares \code{matchWins = 5} and the result screen prints it, +\code{oskiewar.js} declares \code{matchWins = 5} and the result screen prints it, but nothing accumulates round wins toward it: self-play calls \code{startSelfPlay} again at every round end, and normal play returns to the title. The replay store agrees --- the operator's audit found all 464 stored diff --git a/papers/arxiv-oskiewar-reels/references.bib b/papers/arxiv-oskiewar-reels/references.bib index ead30a5a00..d01ca3b4f3 100644 --- a/papers/arxiv-oskiewar-reels/references.bib +++ b/papers/arxiv-oskiewar-reels/references.bib @@ -31,7 +31,7 @@ } @misc{acrepo, - title={{Aesthetic Computer source repository: \texttt{xbox/live/marketing/}, \texttt{xbox/live/hello.js}, \texttt{xbox/live/mac-test.html}}}, + title={{Aesthetic Computer source repository: \texttt{xbox/live/marketing/}, \texttt{xbox/live/oskiewar.js}, \texttt{xbox/live/mac-test.html}}}, author={{Aesthetic Computer contributors}}, year={2026}, url={https://github.com/whistlegraph/aesthetic-computer}, diff --git a/papers/arxiv-oskiewar-store/store.tex b/papers/arxiv-oskiewar-store/store.tex index 3fe6447cf2..bf0f2b47c4 100644 --- a/papers/arxiv-oskiewar-store/store.tex +++ b/papers/arxiv-oskiewar-store/store.tex @@ -121,7 +121,7 @@ The long answer is the rest of this report. It is a research finding rather than Two bodies of evidence, and one deliberate exclusion. -The artifact side is the repository at commit \code{2fcb1b6f4}~\cite{acrepo}: \code{xbox/\allowbreak native-bios/\allowbreak App.cpp} (2,653 lines), \code{Package.\allowbreak appxmanifest}, the Visual C++ project files, \code{xbox/\allowbreak live/\allowbreak hello.js} (5,813 lines), \code{xbox/\allowbreak native-bios/\allowbreak smoke\allowbreak\_piece.js} (12 lines), \code{xbox/tools/}, and \code{xbox/ota/}. Claims about what the app does are grep-checked against that source rather than against memory or prior notes. Where the internal research note \code{xbox/PUBLISHING.md} \cite{acpublishing} and the source disagree --- it records \code{hello.js} at 5,549 lines --- the source wins and the divergence is stated. +The artifact side is the repository at commit \code{2fcb1b6f4}~\cite{acrepo}: \code{xbox/\allowbreak native-bios/\allowbreak App.cpp} (2,653 lines), \code{Package.\allowbreak appxmanifest}, the Visual C++ project files, \code{xbox/\allowbreak live/\allowbreak oskiewar.js} (5,813 lines), \code{xbox/\allowbreak native-bios/\allowbreak smoke\allowbreak\_piece.js} (12 lines), \code{xbox/tools/}, and \code{xbox/ota/}. Claims about what the app does are grep-checked against that source rather than against memory or prior notes. Where the internal research note \code{xbox/PUBLISHING.md} \cite{acpublishing} and the source disagree --- it records \code{oskiewar.js} at 5,549 lines --- the source wins and the divergence is stated. The policy side is Microsoft-owned pages only, each fetched and read in full on 7 August 2026: the Microsoft Store Policies document, the Xbox Requirements (XR) list for console games, individual XR pages, the game-publishing onboarding guide, the two registration-fee notices, the UWP platform pages, the Developer Mode activation terms, and the Xbox Accessibility Guidelines. Where a page carries its own version and effective date, both are recorded in the bibliography alongside the access date, because several of these documents are versioned independently of the day they were last touched. @@ -146,7 +146,7 @@ Language & C++/CX (\code{CompileAsWinRT}), toolset v143, x64 only \\ Capabilities & \code{internetClient}, \code{private\-Network\-Client\-Server}, \code{removable\-Storage}, \code{optical} \\ Runtime & QuickJS-ng vendored \cite{quickjs}; D3D11 at a fixed 1920$\times$1080 backbuffer; XAudio2 \\ Piece in package & \code{smoke\_piece.js}, 12 lines \\ -Piece at runtime & \code{hello.js}, 5,813 lines, delivered out of band \\ +Piece at runtime & \code{oskiewar.js}, 5,813 lines, delivered out of band \\ Signing & Self-signed \code{CN=Aesthetic\-Computer\-Dev}; \code{SideloadOnly} MSIX \\ \bottomrule \end{tabularx} @@ -227,14 +227,14 @@ Image decode & Windows Imaging & Own decoder or middleware \\ Script engine & QuickJS-ng, vendored & Ports unchanged \\ Rendering & D3D11, six HLSL shaders & Ports unchanged \\ Audio & XAudio2 graph & Ports unchanged \\ -Game logic & \code{hello.js}, 5,813 lines & Ports unchanged \\ +Game logic & \code{oskiewar.js}, 5,813 lines & Ports unchanged \\ \bottomrule \end{tabularx} \end{table*} That division is the good news buried in the finding. The parts of \game{} that are actually \game{} --- the fixed-step simulation, the pose and hitbox geometry, the camera, the synthesis, the replay format --- live above the boundary and do not care which operating system is underneath. The parts that have to be rewritten are the parts that were never the work. It is the same seam the web spectator already proved: one deterministic core, platform adapters at the edges \cite{scudder2026pieces,scudder2026os}. A GDK host is a third adapter. -The narrow question worth answering before committing to any of it is whether QuickJS, the D3D11 renderer, and \code{hello.js} boot at all under GameCore x64. If they do, the remainder is API substitution with a known shape. If they do not, that is the moment to stop. +The narrow question worth answering before committing to any of it is whether QuickJS, the D3D11 renderer, and \code{oskiewar.js} boot at all under GameCore x64. If they do, the remainder is API substitution with a known shape. If they do not, that is the moment to stop. \section{What Fails Certification} @@ -336,9 +336,9 @@ Table~\ref{tab:cost} sequences the work. Nothing before step 4 requires a decisi \# & Step & Cost / time \\ \midrule 1 & Measure Edge on a retail console: gamepad mapping and latency at \code{oskiewar.com} & \$0 / 1 hour \\ -2 & Bake \code{hello.js} into the package; compile \code{PollLivePiece()} out of release builds & \$0 / 1--2 days \\ +2 & Bake \code{oskiewar.js} into the package; compile \code{PollLivePiece()} out of release builds & \$0 / 1--2 days \\ 3 & Register at \code{store\-developer.\-microsoft.\-com} --- company account, new dedicated MSA & \$0 / 2--5 business days \\ -4 & Install the GDK; spike the platform layer. Does QuickJS + D3D11 + \code{hello.js} boot under GameCore x64? & \$0 / 1--2 weeks \\ +4 & Install the GDK; spike the platform layer. Does QuickJS + D3D11 + \code{oskiewar.js} boot under GameCore x64? & \$0 / 1--2 weeks \\ 5 & Ship the PC build. IARC, privacy policy, packaging, the certification loop & \$0 / 2--4 weeks \\ 6 & Apply to ID@Xbox with a concept & \$0 / 10--15 business days, up to 3 weeks \\ 7 & Achievements, Xbox user model, name layer, privilege checks, suspend/resume, Recent Player reporting & devkit / 1--2 months \\ diff --git a/papers/arxiv-oskiewar/oskiewar.tex b/papers/arxiv-oskiewar/oskiewar.tex index e54a9830fe..e2f5385701 100644 --- a/papers/arxiv-oskiewar/oskiewar.tex +++ b/papers/arxiv-oskiewar/oskiewar.tex @@ -103,7 +103,7 @@ The result remains a laboratory rather than a claim to a finished competitive fi \section{System Architecture} -The system has four layers (Figure~\ref{fig:architecture}). The Xbox shell owns Windows.Gaming.Input, rendering, audio, storage, HTTPS, UDP, and WebSocket transports. A QuickJS context runs \code{xbox/live/hello.js}. The script owns match rules and native presentation. A relay accepts latest-only spectator snapshots, the web piece renders them with the standard Aesthetic Computer lifecycle, and the monolith stores bounded replay demos. Ableton Live receives redundant OSC datagrams over the local network. +The system has four layers (Figure~\ref{fig:architecture}). The Xbox shell owns Windows.Gaming.Input, rendering, audio, storage, HTTPS, UDP, and WebSocket transports. A QuickJS context runs \code{xbox/live/oskiewar.js}. The script owns match rules and native presentation. A relay accepts latest-only spectator snapshots, the web piece renders them with the standard Aesthetic Computer lifecycle, and the monolith stores bounded replay demos. Ableton Live receives redundant OSC datagrams over the local network. \begin{figure*}[t] \centering @@ -117,7 +117,7 @@ The system has four layers (Figure~\ref{fig:architecture}). The Xbox shell owns arrow/.style={-{Latex[length=2mm]},thick,draw=acdark!70}] \node[box] (pads) {Two Xbox\\controllers}; \node[core,right=of pads] (host) {UWP native host\\v1.0.0.35}; - \node[core,right=of host] (piece) {QuickJS piece\\\code{hello.js}}; + \node[core,right=of host] (piece) {QuickJS piece\\\code{oskiewar.js}}; \node[box,above=of piece] (render) {D3D11 renderer\\XAudio2 synth}; \node[net,right=of piece] (relay) {Match relay\\WSS, latest-only}; \node[net,below=of relay] (phone) {Web spectator\\\code{/oskiewar:match}}; @@ -218,7 +218,7 @@ The present browser piece is deliberately a spectator, not a second implementati \label{fig:web-spectator} \end{figure} -Playable web parity should not maintain a second copy of the rules. The next architecture extracts quantized commands, fixed-step simulation, collision geometry, pose state, replay serialization, and camera targets from \code{xbox/live/hello.js} into shared modules. Native and web adapters supply input, drawing, sound, persistence, and transport. Keyboard and browser gamepads map onto the same deterministic command core. +Playable web parity should not maintain a second copy of the rules. The next architecture extracts quantized commands, fixed-step simulation, collision geometry, pose state, replay serialization, and camera targets from \code{xbox/live/oskiewar.js} into shared modules. Native and web adapters supply input, drawing, sound, persistence, and transport. Keyboard and browser gamepads map onto the same deterministic command core. \section{Network and Capabilities} diff --git a/xbox/GDK-PORT.md b/xbox/GDK-PORT.md index 3758922348..dc088d23af 100644 --- a/xbox/GDK-PORT.md +++ b/xbox/GDK-PORT.md @@ -51,7 +51,7 @@ across twelve WinRT namespaces. The inventory: What crosses untouched is more than it looks. `QuickJsEngine.cpp` — 873 lines, the entire interpreter binding — contains **zero** WinRT references. All six HLSL shaders, the D3D11 render graph, the XAudio2 voice graph and its DSP, the image -effects in `xbox/runtime/`, and `hello.js` are portable. Storage is nearly free +effects in `xbox/runtime/`, and `oskiewar.js` are portable. Storage is nearly free because every read is already `_wfopen_s` on a wide path; only *acquiring* the path is WinRT. @@ -350,9 +350,9 @@ rulebook; that framing should go. What survives is better than nothing: the retail title can legitimately pull **data** after release through Global Title Storage or PlayFab Title Data — new fighter rosters, tuning constants, seasonal palettes, event schedules, the AC -community feed — as long as it is data the shipped `hello.js` interprets, not a -replacement for `hello.js`. If the live loop matters enough, the design move is -to widen what `hello.js` reads as data, so a large fraction of what is currently +community feed — as long as it is data the shipped `oskiewar.js` interprets, not a +replacement for `oskiewar.js`. If the live loop matters enough, the design move is +to widen what `oskiewar.js` reads as data, so a large fraction of what is currently hot-reloaded as script becomes content the certified runtime consumes. On PC the calculus differs, because only 10.2.2 applies and it is intent-based. @@ -409,12 +409,12 @@ being passed. ### Getting the game into the package Landed, and it is the smallest high-value change here. `App.cpp` now reads -`hello.js` out of the package at boot through the existing `ReadPackageBytes`, +`oskiewar.js` out of the package at boot through the existing `ReadPackageBytes`, and falls back to `kSmokePiece` only if the file is absent. `NativeBios.vcxproj` -adds `..\live\hello.js` as Content, alongside the shaders and the fonts. +adds `..\live\oskiewar.js` as Content, alongside the shaders and the fonts. Package Content rather than a compiled-in byte array, for three reasons. MSVC -rejects string literals over 65,535 characters and `hello.js` is 237 KB, so the +rejects string literals over 65,535 characters and `oskiewar.js` is 237 KB, so the obvious raw-string approach does not compile. Package content is signed, immutable, and Store-delivered, which is precisely what 10.2.5 asks for. And it keeps the game a file in the repo that the existing tooling can still edit. If @@ -425,7 +425,7 @@ package is the unit the Store signs and distributes. ### CI `.github/workflows/xbox-gdk-desktop.yml`, on `windows-2022`, on any push -touching `xbox/native-bios/**`, `xbox/runtime/**` or `xbox/live/hello.js` — +touching `xbox/native-bios/**`, `xbox/runtime/**` or `xbox/live/oskiewar.js` — the game is package content now, so it belongs in the trigger. GitHub Actions rather than AppVeyor, for one reason: AppVeyor's config here is a @@ -446,7 +446,7 @@ rather than gates, because without the Xbox Extensions it cannot be conclusive. **The retail dev/live split described above is still design, not code.** The `AcDevLivePiece` property and the `AC_DEV_LIVE_PIECE` define exist in both projects and the GDK one defaults to `0`, but the string scan over a built -package and the `hello.js` size assertion are not written, and the WinRT lane's +package and the `oskiewar.js` size assertion are not written, and the WinRT lane's dev/retail jobs are not written. That is a separate change against `appveyor.yml` and should be one. @@ -593,9 +593,9 @@ and run. **Changed in the previous pass, still not compiler-checked** (the UWP lane builds on AppVeyor, so `main` being green covers these, but no one has looked): -the `App.cpp:1806` clock fix, boot reading `hello.js` out of the package, the +the `App.cpp:1806` clock fix, boot reading `oskiewar.js` out of the package, the `#if AC_DEV_LIVE_PIECE` guards, and `NativeBios.vcxproj`'s `AcDevLivePiece` -property and `..\live\hello.js` Content entry. +property and `..\live\oskiewar.js` Content entry. **Deliberately left alone:** `smoke_piece.js` and its Content entry, which are dead; the `xbox/ota/` lane; `xbox/live/**`; and the 2,678 lines of `App.cpp` diff --git a/xbox/OSKIEWAR-RELEASE.md b/xbox/OSKIEWAR-RELEASE.md new file mode 100644 index 0000000000..a7b20de252 --- /dev/null +++ b/xbox/OSKIEWAR-RELEASE.md @@ -0,0 +1,21 @@ +# Oskiewar release + +`npm run oskiewar:deploy` is the only canonical live release command. It +fingerprints `xbox/live/oskiewar.js`, refuses uncommitted game source, records +an obligation for web, iOS, and Xbox, deploys the web first, verifies its +production bytes, then updates Xbox through Device Portal. + +iOS loads that verified web game and polls every two seconds, with its bundled +copy as an offline fallback. Game-only updates therefore need no App Store or +device rebuild. Changes under `apple/oskiewar` or `xbox/native-bios` escalate +the receipt to a native refresh instead of claiming false parity. + +Use `npm run oskiewar:parity` to inspect the current fingerprint and all three +channel states. If a device or service was unavailable, the failed or pending +state remains in `.git/oskiewar-parity.json`; run +`npm run oskiewar:reconcile` when it returns. A new source fingerprint creates +a new set of obligations without erasing the last channel receipts. + +Direct canonical Xbox source deployment is blocked by `xbox/tools/live.mjs`. +Diagnostic pieces and non-Oskiewar experiments remain available through that +tool. diff --git a/xbox/PUBLISHING.md b/xbox/PUBLISHING.md index e56d44d6fd..f81f72af5c 100644 --- a/xbox/PUBLISHING.md +++ b/xbox/PUBLISHING.md @@ -21,11 +21,11 @@ The port is not a recompile. `xbox/native-bios/App.cpp` is C++/CX against `CoreWindow`, `ApplicationData`, `Windows.Gaming.Input`, `Windows.Web.Http`, `Windows.Networking.Sockets`, `Windows.Devices.Midi`, and WinRT `StorageFile`. GDK titles are plain x64 Win32 with a different API surface for every one of -those. The QuickJS core, the renderer's HLSL, the DSP, and `hello.js` port +those. The QuickJS core, the renderer's HLSL, the DSP, and `oskiewar.js` port cleanly; the platform layer is a rewrite. Two things would fail certification even after a clean port. The game is not in -the package — only `smoke_piece.js` ships, and `hello.js` arrives through the +the package — only `smoke_piece.js` ships, and `oskiewar.js` arrives through the `live-piece.js` sideload path, which cannot exist on retail. And the app has zero Xbox platform integration: no achievements (mandatory, minimum ten and 1000 gamerscore), no Xbox user model, no privilege checks, and it displays AC @@ -46,7 +46,7 @@ doing, but as a deliberate multi-month project, not a packaging step. - **Runtime:** QuickJS-ng v0.15.1 vendored, 124 bound globals, D3D11 renderer at a fixed 1920×1080 backbuffer scaled to output, XAudio2. - **Shipped piece:** `smoke_piece.js`, twelve lines — wipe, four button colors, a - beep. `hello.js` (5,549 lines, the actual game) is not in the package. + beep. `oskiewar.js` (5,549 lines, the actual game) is not in the package. - **Piece delivery:** `PollLivePiece()` stats `LocalState\live-piece.js` every 500 ms and hot-swaps on change. `xbox/tools/live.mjs` writes that file over Device Portal. The signed-manifest OTA design in `xbox/ota/` has no UWP adapter @@ -155,12 +155,12 @@ door is shut. Everything below assumes the GDK port happens first. Windows Imaging. GDK titles are standard x64 Win32. Every one of those needs a replacement — `XGameSave`/`XPersistentLocalStorage`, `GameInput`, `libHttpClient` or WinHTTP, Winsock. The QuickJS engine, the six HLSL shaders, the audio graph, -and `hello.js` are portable; `App.cpp`'s platform layer is not. +and `oskiewar.js` are portable; `App.cpp`'s platform layer is not. -**The game is not in the package.** CI packages `smoke_piece.js`. `hello.js` +**The game is not in the package.** CI packages `smoke_piece.js`. `oskiewar.js` reaches the console only through `PollLivePiece()` reading `LocalState\live-piece.js`, written by Device Portal, which does not exist on -retail. A Store build ships a twelve-line color demo. `hello.js` has to be baked +retail. A Store build ships a twelve-line color demo. `oskiewar.js` has to be baked in as a package asset, and `PollLivePiece()` has to be compiled out of release builds — not merely inert, removed. @@ -294,7 +294,7 @@ Nothing before step 4 requires a decision about the port, and nothing before ste answer to "people can play it on their own Xbox" available this week, and it changes how urgent everything below is. -2. **Bake `hello.js` into the package and delete the sideload path from release +2. **Bake `oskiewar.js` into the package and delete the sideload path from release builds.** One to two days. This is required for every route, is independently correct, and turns the MSIX into an actual game rather than a shell. Keep `PollLivePiece()` behind a debug configuration so the dev loop survives. @@ -309,7 +309,7 @@ Nothing before step 4 requires a decision about the port, and nothing before ste 4. **Install the GDK and spike the platform layer.** One to two weeks. Public, no NDA, `winget`. The question to answer is narrow: does QuickJS plus the D3D11 - renderer plus `hello.js` boot under GameCore x64? If it does, the port is + renderer plus `oskiewar.js` boot under GameCore x64? If it does, the port is bounded and the rest is API substitution. If it does not, that is the moment to reconsider. diff --git a/xbox/live/MARKETING.md b/xbox/live/MARKETING.md index 08a4037af5..7d207fc58a 100644 --- a/xbox/live/MARKETING.md +++ b/xbox/live/MARKETING.md @@ -35,7 +35,7 @@ robots sparring; which round is still arithmetic — the seed indexes the filtered list. If the store is unreachable or has nothing over seven seconds, it falls back to self-play and says so. -Seeding works because `hello.js` calls `Math.random` **exactly once**, to seed +Seeding works because `oskiewar.js` calls `Math.random` **exactly once**, to seed the match name; everything downstream — round names, ball kind, the fighters — falls out of it. `render.mjs` replaces `Math.random` with a seeded generator before the game boots. @@ -49,7 +49,7 @@ renderer is moved onto a fixed timestep. ### 2 · Render — the real game, at the real shape -Headless Chrome runs `mac-test.html` + `hello.js` through the same frame driver +Headless Chrome runs `mac-test.html` + `oskiewar.js` through the same frame driver a player gets. Two halves, both borrowed from `marketing/av-reels`: - **Video** — CDP `Page.startScreencast` → timestamped JPEG frames, concatenated @@ -80,7 +80,7 @@ result card. Head and tail are trimmed; nothing inside is cut. > Verified by eye on the first and last frames — the reel opens on its own > countdown and closes on its own card. -> **What "a match" means here.** `hello.js` carries `matchWins = 5`, but +> **What "a match" means here.** `oskiewar.js` carries `matchWins = 5`, but > nothing accumulates round wins toward it — self-play calls `startSelfPlay` > again at every round end and normal play returns to the title. All 464 rounds > in the replay store have `roundIndex: 0`. So a match *is* a round today: diff --git a/xbox/live/hello.js b/xbox/live/hello.js deleted file mode 100644 index 3af2ca3c88..0000000000 --- a/xbox/live/hello.js +++ /dev/null @@ -1,7360 +0,0 @@ -// @bundle-qr -// The console's monotonic clock can hand back a negative number: App.cpp -// converts QPC ticks with `counter * 1000000`, which overflows int64 past -// about ten days of uptime. Every deadline in this piece starts at 0 and is -// compared with `now >=` or `now <`, so a negative clock made `hitStunUntil` -// permanently in the future -- no attack could fire and no round could be -// won -- stranded the title, silenced the input log, and indexed off the -// front of a palette. Rebase once, here, so time starts at zero whatever the -// host reports. The host builds a fresh object per call, so this rewrites -// that copy rather than allocating another. -const hostRuntime = runtime; -let clockEpoch = null; -runtime = function acRuntime() { - const info = hostRuntime(); - const raw = Number(info.monotonicUs) || 0; - if (clockEpoch === null) clockEpoch = raw; - info.monotonicUs = raw - clockEpoch; - if (typeof info.simMonotonicUs === "number") - info.simMonotonicUs -= clockEpoch; - return info; -}; - -const buildTimestamp = "2026.08.07.1336 PDT"; -const floorY = 12000; -const ceilingY = 0; -const wallThickness = 80; -const worldLeft = 0; -const worldRight = 12000; -const worldNear = -1800; -const worldFar = 1800; -const stageLeft = 0; -let stageRight = 1920; -let stageTop = 112; -// Leave a narrow projection gutter beneath the floor for the screen-edge HUD. -let stageBottom = 930; -let viewHeight = 1080; -let cameraAspect = (stageRight - stageLeft) / (stageBottom - stageTop); -// The stage furniture is flagged out for now — @jeffrey wants the arena to -// read as one floor. Flip PLATFORM true and the ledge comes back whole: -// drawing, collision, pickup lanes, grenade and ball bounces, the bot's -// sink-and-chase play. The wind flag rides its own switch the same way. -const PLATFORM = false; -const WIND_FLAG = false; -const platformLeft = 4500; -const platformRight = 7500; -// A jump apexes 290 above the floor, so the ledge sits inside a single hop and -// inside the tight opening frame. At its old 1600 only an ultra jump reached -// it, which put every platform powerup out of play and off screen. -const platformY = 11760; -const doubleTapUs = 280000; -const doubleTapReleaseUs = 40000; -const roundDurationUs = 30000000; -const roundResultUs = 3000000; -const matchResultUs = 5000000; -const introDurationUs = 3000000; -const dummyGuideDurationUs = 150000000; -const matchWins = 5; -const errorRestartUs = 16000000; -const errorDumpBase = "https://oskiewar.com/api/oskiewar-dump?d="; -const powerupIntervalUs = 10000000; -const shieldRadius = 160; -const shieldForward = 30; -const grenadeBlastDuration = .68; -const grenadeBlastRadius = 620; -// Ground pound. Double-tapping DOWN in the air used to be a plain fast drop -// that did nothing on arrival; now it commits you. Holding DOWN through the -// fall buys speed, the crater scales with how far you actually fell, and the -// landing balls you — so the move is a trade, never a free hit. -const poundLaunchVelocity = 1500; -const poundHoldAcceleration = 5600; -const poundMaxVelocity = 4600; -// Roughly a jump-and-a-half of floor-to-ceiling travel. Falling further than -// this cannot buy a bigger crater, so the ceiling is not a weapon. -const poundFullFall = 900; -const poundMinRadius = 240; -const poundMaxRadius = 760; -const replayTickUs = 16667; -const replayCheckpointUs = 1000000; -const liveSnapshotIntervalUs = 50000; -const fighterAnimationSpecs = { - // WALK ran a whole stride in 12 ticks — five cycles a second, which the eye - // reads as a vibrating fighter rather than as legs taking steps. Three ticks - // a frame puts the cycle at 600ms, close to a real walk, so the steps count. - IDLE: [48, 2, "BREATHE", true], WALK: [12, 3, "STRIDE", true], - RUN: [10, 1, "RUN", true], MEDITATE: [24, 2, "STILL", true], - DASH: [8, 1, "BURST", true], CROUCH: [8, 1, "TUCK", true], - "AIR CROUCH": [8, 1, "TUCK", true], JUMP: [12, 1, "ASCEND", true], - FALL: [12, 1, "DESCEND", true], PUNCH: [14, 1, "ATTACK", false], - KICK: [14, 1, "ATTACK", false], SHIELD: [12, 2, "GUARD", true], - REACH: [10, 1, "REACH", false], HOLD: [16, 2, "CLUTCH", true], - FIRE: [11, 1, "FIRE", false], THROW: [16, 1, "THROW", false], - WHIP: [11, 1, "ATTACK", false], BASH: [17, 1, "ATTACK", false], - HIT: [10, 1, "STUN", false], POGO: [12, 1, "BOUNCE", true], - "CROUCH HOP": [8, 1, "HOP", true], SINK: [10, 1, "SINK", true], - KO: [16, 2, "BREAK", false], -}; -// Every melee kind in one table: how far the limb reaches at rest and at full -// extension, how long each of its two bones is (the real reach ceiling), how -// high it lands, how wide it reads against the ball, how long the hitbox -// lives, and what it does on contact. A loaded hand cannot make a clean fist, -// so an item swing is its own kind rather than a punch modifier — a pistol -// lengthens the arm into a fast light lash, a grenade shortens it into a club. -const meleeSpecs = { - PUNCH: { reach: 58, swell: 50, span: 58, height: 115, radius: 28, - windowUs: 220000, force: 1200, lift: 140, - cue: ["snare", 1.05], thud: ["block", 1] }, - KICK: { reach: 75, swell: 62, span: 74, height: 55, radius: 35, - windowUs: 220000, force: 1550, lift: 220, - cue: ["kick", 1.05], thud: ["block", 1] }, - WHIP: { reach: 92, swell: 74, span: 76, height: 122, radius: 24, - windowUs: 190000, force: 1000, lift: 110, - cue: ["whoosh", 1.15], thud: ["hat", 1.35] }, - BASH: { reach: 62, swell: 40, span: 58, height: 108, radius: 40, - windowUs: 280000, force: 1750, lift: 210, - cue: ["kick", 1.3], thud: ["kick", 1.4] }, -}; -// One lookup serves both Y and the loaded punch, so the thing you can see in -// the fighter's hand is always the thing that fires and the thing that swings. -const itemMelee = { GUN: "WHIP", GRENADE: "BASH" }; -const instantReplayStepUs = 33333; -// Replay speed ramping: how far a full-force hit drags the playhead down, how -// many frames of warning the ramp gets, and how fast the speed itself may -// change — the last one is what keeps a slowdown from reading as a stutter. -const replaySlowest = .18; -const replayActionLead = 8; -const replayRampPerSecond = 3.2; -const instantReplayMaxFrames = 240; -const walkSpeed = 1060; -const runStartSpeed = 1320; -const runTopSpeed = 2350; -const runAcceleration = 820; -// Vertical feel. The apex is the design constant — how high a fighter can -// reach never changed — so every impulse here is paired with a gravity that -// spends less time getting there. Rise is lighter than fall so the arc reads -// as intent going up and commitment coming down. -const riseGravity = 4800; -const fallGravity = 7200; -const jumpVelocity = 1760; -const crouchJumpVelocity = 1960; -const ultraJumpVelocity = 3960; -const crouchHopVelocity = 980; -const pogoBounceVelocity = 2100; -const headBounceVelocity = 1100; -const jumpAnticipationUs = 50000; -// Letting go of up mid-rise snips the arc: a tap is a hop, a hold is a jump. -const jumpCutScale = .55; -const jumpPoseUs = 75000; -const crouchJumpPoseUs = 145000; -const crouchHopPoseUs = 190000; -const sinkDurationUs = 250000; -const hudTypeSize = 42; -// The command stream is a record of what you just did, so it is long enough to -// hold a whole exchange and it does not start dissolving while you are still -// playing. Only once the pad goes quiet does it age out, oldest glyph first, -// so the last thing you pressed is the last thing to leave. -const commandStreamDepth = 20; -const commandStreamRows = 5; -const commandStreamColumns = 8; -const commandHoldUs = 1100000; -const commandFadeUs = 1900000; -const replayButtons = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", - "A", "B", "X", "Y"]; -let cameraCenter = (worldLeft + worldRight) / 2; -let cameraWidth = worldRight - worldLeft; -let cameraCenterY = floorY - cameraWidth / cameraAspect / 2; -let cameraContainFloor = 0; -// Two different distances, because they answer two different questions. -// -// `cameraPin` is where a single projected point stops being allowed to get any -// nearer. Nothing can clip a point — a shadow ellipse or a capsule end is one -// coordinate, not a face — so the best available answer is to hold it at a -// sane depth and accept that a thing behind the lens is drawn wrong. This is -// the distance the projection has always used. -const cameraPin = 80; -// `cameraNear` is the real near plane, and faces are cut at it. It is much -// closer than the pin on purpose. The cut is a straight line at constant -// depth, and where that line lands on screen is decided by how much the lens -// magnifies at the plane -- and this lens is only a tenth perspective in -// normal play, which dilutes that magnification tenfold. Clipping the floor at -// the pin distance put its cut line just inside the bottom of the frame and -// opened a band of sky under the fighters' feet. Ten times nearer drives the -// cut off screen for any camera that is meaningfully above the floor, and the -// coordinates it asks for in exchange are what the guard band is for. -const cameraNear = 8; -// Projected faces are cut to a band one viewport wide on every side before -// they are handed over. The scene is allowed to run off screen; it is not -// allowed to run off into coordinates the rasterizer cannot hold, which is -// what a vertex sitting on the near plane will otherwise ask for. -const guardBand = 1; -const clamp = (value, low, high) => Math.max(low, Math.min(high, value)); -const mixColor = (dark, light, amount) => dark.map((value, index) => - Math.round(value + (light[index] - value) * amount)); -const lerp = (from, to, amount) => from + (to - from) * amount; -const normalize3 = (point) => { - const length = Math.hypot(point.x, point.y, point.z) || 1; - return { x: point.x / length, y: point.y / length, z: point.z / length }; -}; -const globalLight = normalize3({ x: -.42, y: 1, z: -.28 }); -const cross3 = (a, b) => ({ x: a.y * b.z - a.z * b.y, - y: a.z * b.x - a.x * b.z, z: a.x * b.y - a.y * b.x }); -const dot3 = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z; -const viewWidth = () => stageRight; -const viewCenterX = () => (stageLeft + stageRight) / 2; -const viewOffsetX = () => (stageRight - 1920) / 2; -const compactLayout = () => stageRight < 1500; -// Keep the HUD inside a traditional five-percent title-safe frame. On a -// 16:9 television this is deliberately deeper than the action-safe camera so -// a bezel, soundbar, or object in front of the screen does not hide status. -const hudSafeInset = () => clamp( - Math.round(Math.min(stageRight, viewHeight) * .05), - compactLayout() ? 24 : 48, 72); -const hudSafeRect = () => { - const inset = hudSafeInset(); - return { left: stageLeft + inset, top: inset, - right: stageRight - inset, bottom: viewHeight - inset }; -}; -const actionSafeRect = () => { - const marginX = compactLayout() ? 34 : 64; - const marginY = 26; - return { left: stageLeft + marginX, top: stageTop + marginY, - right: stageRight - marginX, bottom: stageBottom - marginY }; -}; - -function syncGameView() { - const next = typeof gameView === "function" ? gameView() : null; - const width = clamp(Math.round(Number(next?.width) || 1920), 480, 2880); - const height = clamp(Math.round(Number(next?.height) || 1080), 480, 2160); - const inputFamily = typeof capabilities === "function" - ? capabilities().inputFamily : "xbox"; - const touch = inputFamily === "touch"; - const compact = width < 1500; - const inset = compact ? 22 : 30; - const nextTop = Math.max(82, inset + hudTypeSize + 16); - const bottomReserve = touch - ? clamp(height * .36, 300, 390) - : clamp(height * .13, 112, 150); - const nextBottom = Math.max(nextTop + 280, height - bottomReserve); - if (width === stageRight && height === viewHeight && - nextTop === stageTop && nextBottom === stageBottom) return; - stageRight = width; - viewHeight = height; - stageTop = nextTop; - stageBottom = nextBottom; - cameraAspect = (stageRight - stageLeft) / (stageBottom - stageTop); - if (typeof cameraDoll !== "undefined") cameraDoll.dirty = true; -} - -class FightCamDoll { - constructor() { - this.position = { x: cameraCenter, y: cameraCenterY, z: -cameraWidth * 1.4 }; - this.target = { x: cameraCenter, y: cameraCenterY, z: 0 }; - this.width = cameraWidth; - this.perspective = 0; - this.fov = 55; - this.roll = 0; - this.dirty = true; - this.view = null; - } - - track(spec, dt, speed = 5) { - // Exponential easing is continuous across frame rates. Move the dolly and - // its look target together so follow motion cannot shear into a sudden - // swivel when fighters cross a framing threshold. - const amount = 1 - Math.exp(-Math.max(0, dt) * speed); - const previousTarget = { ...this.target }; - for (const axis of ["x", "y", "z"]) - this.target[axis] = lerp(this.target[axis], spec.target[axis], amount); - for (const axis of ["x", "y", "z"]) - this.position[axis] += this.target[axis] - previousTarget[axis]; - for (const axis of ["x", "y", "z"]) { - this.position[axis] = lerp(this.position[axis], spec.position[axis], amount); - } - // Pull back promptly but never in a single frame; return close more - // slowly so small changes at the action-safe edge do not pump the lens. - const zoomSpeed = spec.width > this.width ? speed * 1.9 : speed * .58; - const zoomAmount = 1 - Math.exp(-Math.max(0, dt) * zoomSpeed); - this.width = lerp(this.width, spec.width, zoomAmount); - this.perspective = lerp(this.perspective, spec.perspective, amount); - this.fov = lerp(this.fov, spec.fov || 55, amount); - this.roll = lerp(this.roll, spec.roll || 0, amount); - this.dirty = true; - } - - prepare() { - const forward = normalize3({ x: this.target.x - this.position.x, - y: this.target.y - this.position.y, z: this.target.z - this.position.z }); - const baseRight = normalize3(cross3(forward, { x: 0, y: -1, z: 0 })); - const baseUp = normalize3(cross3(baseRight, forward)); - const rollCos = Math.cos(this.roll); - const rollSin = Math.sin(this.roll); - const right = { x: baseRight.x * rollCos + baseUp.x * rollSin, - y: baseRight.y * rollCos + baseUp.y * rollSin, - z: baseRight.z * rollCos + baseUp.z * rollSin }; - const up = { x: baseUp.x * rollCos - baseRight.x * rollSin, - y: baseUp.y * rollCos - baseRight.y * rollSin, - z: baseUp.z * rollCos - baseRight.z * rollSin }; - this.view = { forward, right, up, - centerX: (stageLeft + stageRight) / 2, - centerY: (stageTop + stageBottom) / 2, - orthoScale: (stageRight - stageLeft) / this.width, - focal: (stageRight - stageLeft) / - (2 * Math.tan(this.fov * Math.PI / 360)) }; - this.dirty = false; - } - - // World space to camera space, with the near distance left alone. Anything - // that wants to survive contact with the near plane has to see the honest - // depth first — `project` is where it gets clamped, and a clamp is a lie for - // anything behind the lens. - toView(point) { - if (this.dirty || !this.view) this.prepare(); - const { forward, right, up } = this.view; - const delta = { x: point.x - this.position.x, y: point.y - this.position.y, - z: point.z - this.position.z }; - return { x: dot3(delta, right), y: dot3(delta, up), - z: dot3(delta, forward) }; - } - - projectView(view) { - if (this.dirty || !this.view) this.prepare(); - const { centerX, centerY, orthoScale, focal } = this.view; - const depth = Math.max(cameraNear, view.z); - const orthoX = centerX + view.x * orthoScale; - const orthoY = centerY - view.y * orthoScale; - const perspectiveX = centerX + view.x * focal / depth; - const perspectiveY = centerY - view.y * focal / depth; - return { x: lerp(orthoX, perspectiveX, this.perspective), - y: lerp(orthoY, perspectiveY, this.perspective), - z: clamp(depth / 16000 * 2.8 - 1.4, -1.4, 1.4) }; - } - - // A lone point cannot be clipped, so it is held at the pin rather than - // allowed to divide by a depth it does not have. Faces go through - // `worldTriangle`, which cuts them at the near plane and then comes back - // here with vertices that are already in front of it. - project(point) { - const view = this.toView(point); - view.z = Math.max(cameraPin, view.z); - return this.projectView(view); - } -} - -const cameraDoll = new FightCamDoll(); -const cameraScale = () => (stageRight - stageLeft) / cameraDoll.width; -let triangleDepth = -1.4; -// A match frame submits ~2100 faces. Buffering them into a Float32Array first -// cost 5.7ms a frame where handing each one straight over costs 1.8ms — -// twelve typed-array element writes in QuickJS are dearer than twelve -// arguments to one C call. The batched host call itself measured 0.05ms, so -// the boundary crossing was never the expense. -const emitTriangle = typeof triangle3d === "function" ? triangle3d - : (x1, y1, z1, x2, y2, z2, x3, y3, z3, r, g, b) => - triangle(x1, y1, x2, y2, x3, y3, r, g, b); -// r/g/b stay positional so the hottest path in the piece doesn't build a rest -// array per face; the host defaults every missing channel to 255 the same way. -// -// The native buffer speaks int16: a coordinate past ±32768 — or the NaN a -// projection returns after crossing the camera plane — is a RangeError and a -// dead frame on console. Geometry that broken was never going to be visible, -// so the seam culls it instead of crashing. NaN fails a comparison on its -// own, which keeps the guard to plain compares on the hottest path. -const triangleSafe = (value) => value > -32200 && value < 32200; -function screenTriangle(x1, y1, x2, y2, x3, y3, r = 255, g = 255, b = 255) { - if (!(triangleSafe(x1) && triangleSafe(y1) && triangleSafe(x2) && - triangleSafe(y2) && triangleSafe(x3) && triangleSafe(y3))) return; - emitTriangle(x1, y1, triangleDepth, x2, y2, triangleDepth, - x3, y3, triangleDepth, r, g, b); -} -function screenRect(x, y, width, height, color) { - const [r, g, b] = color; - screenTriangle(x, y, x + width, y, x + width, y + height, r, g, b); - screenTriangle(x, y, x + width, y + height, x, y + height, r, g, b); -} -function projectedTriangle(a, b, c, color) { - if (!(triangleSafe(a.x) && triangleSafe(a.y) && triangleSafe(a.z) && - triangleSafe(b.x) && triangleSafe(b.y) && triangleSafe(b.z) && - triangleSafe(c.x) && triangleSafe(c.y) && triangleSafe(c.z))) return; - emitTriangle(a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, ...color); -} -// Sutherland-Hodgman, one plane at a time. Both clips below are the same walk: -// step the polygon's edges, keep the vertices that are inside, and whenever an -// edge crosses out or in, keep the crossing point too. -function clipPolygon(polygon, distance, mix) { - const kept = []; - for (let index = 0; index < polygon.length; index++) { - const current = polygon[index]; - const next = polygon[(index + 1) % polygon.length]; - const here = distance(current); - const there = distance(next); - if (here >= 0) kept.push(current); - if ((here >= 0) !== (there >= 0)) - kept.push(mix(current, next, here / (here - there))); - } - return kept; -} -const mixVertex = (a, b, amount) => ({ x: lerp(a.x, b.x, amount), - y: lerp(a.y, b.y, amount), z: lerp(a.z, b.z, amount) }); -// The near plane, in camera space, before the divide. This is the whole point: -// a vertex behind the lens has a negative depth, and pinning it to the near -// distance keeps its sideways offset while dividing by a distance it never -// had — so a face that crosses the plane used to shear halfway across the -// screen, or blow past the coordinate guard and vanish outright. Cutting it at -// the plane leaves only geometry that is honestly in front of the camera. -const clipViewNear = (polygon) => - clipPolygon(polygon, (vertex) => vertex.z - cameraNear, mixVertex); -// And the guard band, in screen space, after it. A vertex sitting exactly on -// the near plane projects to focal/near times its offset — twenty-three times, -// at this lens — so a legitimately visible floor can still ask for coordinates -// far outside anything the rasterizer will take. Cut it to a band a viewport -// wide instead of dropping the face and leaving a hole in the ground. -function clipScreenBand(polygon) { - const width = viewWidth(); - const margin = guardBand; - const edges = [ - (vertex) => vertex.x + width * margin, - (vertex) => width * (1 + margin) - vertex.x, - (vertex) => vertex.y + viewHeight * margin, - (vertex) => viewHeight * (1 + margin) - vertex.y, - ]; - let clipped = polygon; - for (const distance of edges) { - if (clipped.length < 3) return []; - clipped = clipPolygon(clipped, distance, mixVertex); - } - return clipped; -} -// One world-space face, all the way through: to camera space, cut at the near -// plane, projected, cut to the guard band, then fanned into whatever triangles -// are left. Zero of them is a face entirely behind the camera, which is a -// perfectly good answer. -function worldTriangle(a, b, c, color) { - const near = clipViewNear([cameraDoll.toView(a), cameraDoll.toView(b), - cameraDoll.toView(c)]); - if (near.length < 3) return; - const projected = near.map((vertex) => cameraDoll.projectView(vertex)); - if (projected.some((point) => !Number.isFinite(point.x) || - !Number.isFinite(point.y))) return; - // A near-plane fan can be a quad; fanning it before the band clip would - // clip the same interior edge twice, so the band takes the whole polygon. - const face = clipScreenBand(projected); - for (let corner = 2; corner < face.length; corner++) - projectedTriangle(face[0], face[corner - 1], face[corner], color); -} -function projectPoint(x, y, z = 0) { - return cameraDoll.project({ x, y, z }); -} -const screenX = (x, z = 0) => projectPoint(x, cameraCenterY, z).x; -const screenY = (y, z = 0) => projectPoint(cameraCenter, y, z).y; -const panAt = (x, z = 0) => clamp( - (projectPoint(x, cameraCenterY, z).x - viewCenterX()) / - Math.max(1, (stageRight - stageLeft) / 2 - 55), -1, 1); -const panPlayer = (player) => panAt(player.x, player.z); -let visualTheme = { light: 0, sunset: 0 }; - -const fighterRoster = [ - { handle: "@JEFFREY", color: [190, 42, 58], colors: [ - [255,105,190],[111,232,210],[255,105,190],[255,232,92], - [130,150,255],[255,105,190],[111,232,210],[255,232,92]], - mood: "New Media Instruments", - lastChat: "hmm this chat could use some ui love" }, - { handle: "@FIFI", color: [209, 100, 216], colors: [ - [209,100,216],[204,253,71],[35,100,255],[82,67,17],[5,249,137]], - mood: "nap", lastChat: "" }, - { handle: "@OSKIE", color: [38, 82, 176], colors: [ - [255,232,92],[255,105,190],[111,232,210],[255,232,92],[130,150,255],[255,105,190]], - mood: "", lastChat: "" }, - { handle: "@SAT", color: [130, 204, 213], colors: [ - [130,204,213],[161,232,0],[253,122,2],[222,90,205]], - mood: "i hope its not ai generated", lastChat: "meow" }, -]; -// Per-glyph color is how a community @handle carries its owner's identity. -// The house opponents are not people, so they hold one flat color each and -// let the glyph walk fall back to it. -const npcFighter = { handle: "DUMMY", color: [105, 125, 150], colors: [], - mood: "TRAINING DUMMY · NO BOT AI", lastChat: "" }; -const spiderDummyFighter = { handle: "SPIDERDUMMY", color: [76, 88, 108], - colors: [], mood: "GIANT SEGMENTED TRAINING FOE · NO BOT AI", lastChat: "" }; -const botFighter = { handle: "BOT", color: [205, 48, 72], colors: [], - mood: "ANGRY TRAINING BOT", lastChat: "" }; -const peopleFighter = { handle: "PPL", color: [116, 122, 136], colors: [], - mood: "", lastChat: "" }; -// Self-play needs two nameplates nobody can mix up, or the result card reads -// "BOT WINS" with no way to tell which bot won. -const selfPlayFighters = [ - { handle: "BOT 1", color: [205, 48, 72], colors: [], - mood: "MECHANICAL TEST · SELF PLAY", lastChat: "" }, - { handle: "BOT 2", color: [48, 118, 205], colors: [], - mood: "MECHANICAL TEST · SELF PLAY", lastChat: "" }, -]; - -function losAngelesSun() { - const radians = Math.PI / 180; - const days = Date.now() / 86400000 + 2440587.5 - 2451545; - const meanLongitude = (280.46 + .9856474 * days) % 360; - const meanAnomaly = (357.528 + .9856003 * days) % 360; - const eclipticLongitude = meanLongitude + 1.915 * Math.sin(meanAnomaly * radians) + - .02 * Math.sin(2 * meanAnomaly * radians); - const obliquity = 23.439 - .0000004 * days; - const rightAscension = Math.atan2(Math.cos(obliquity * radians) * - Math.sin(eclipticLongitude * radians), Math.cos(eclipticLongitude * radians)) / - radians; - const declination = Math.asin(Math.sin(obliquity * radians) * - Math.sin(eclipticLongitude * radians)); - const siderealHours = 18.697374558 + 24.06570982441908 * days; - let hourAngle = ((siderealHours * 15 - 118.2437 - rightAscension + 540) % 360) - 180; - const latitude = 34.0522 * radians; - const altitude = Math.asin(Math.sin(latitude) * Math.sin(declination) + - Math.cos(latitude) * Math.cos(declination) * - Math.cos(hourAngle * radians)) / radians; - const light = clamp((altitude + 6) / 14, 0, 1); - const sunset = clamp(1 - Math.abs(altitude - 1) / 11, 0, 1) * - (hourAngle > 0 ? 1 : .42); - return { light: light * light * (3 - 2 * light), sunset }; -} - -function displayTheme() { - const sun = losAngelesSun(); - const caps = typeof capabilities === "function" ? capabilities() : {}; - if (caps.platform === "web" || caps.platform === "macos") { - return { ...sun, light: caps.colorScheme === "light" ? 1 : 0 }; - } - return sun; -} -const players = [ - { name: "@JEFFREY", rosterIndex: 0, handleColors: fighterRoster[0].colors, - pad: 0, spawnX: 5700, x: 5700, y: floorY, z: 0, - vx: 0, vy: 0, vz: 0, facing: 1, - grounded: true, ducking: false, previous: [], lastButton: "NONE", - lastButtonAt: 0, color: [190, 42, 58], hit: 0, - hitSegment: -1, hitSegmentUntil: 0, hitStunUntil: 0, - alive: true, respawnAt: 0, score: 0, inputX: 0, inputY: 0, - suppressedDirections: [], - lastTap: {}, lastRelease: {}, dashUntil: 0, dashVx: 0, runSince: 0, roundWins: 0, - attackKind: "", attackStartedAt: 0, - attackUntil: 0, attackHit: false, blocking: false, blockFlash: 0, - shieldLocked: false, shieldBrokenAt: 0, - windVx: 0, knockVx: 0, gunAmmo: 0, grenadeAmmo: 0, - gunAimX: 1, gunAimY: 0, stance: "NEUTRAL", - heldBall: -1, grabHeld: false, crouchBlend: 0, standingOn: -1, - partDamage: {}, removedParts: [], pogoHit: false, pogoDive: false, - pounding: false, poundFrom: 0, - commandStream: [], - botPresses: {}, - jumpLaunchAt: 0, jumpPoseUntil: 0, landPoseUntil: 0, - jumpHeld: false, hopUntil: 0, sinkUntil: 0, - crouchJump: false, attackMomentum: 1 }, - { name: "@OSKIE", rosterIndex: 2, handleColors: fighterRoster[2].colors, - npc: false, bot: false, - pad: 1, spawnX: 6300, x: 6300, y: floorY, z: 0, - vx: 0, vy: 0, vz: 0, facing: -1, - grounded: true, ducking: false, previous: [], lastButton: "NONE", - lastButtonAt: 0, color: [38, 82, 176], hit: 0, - hitSegment: -1, hitSegmentUntil: 0, hitStunUntil: 0, - alive: true, respawnAt: 0, score: 0, inputX: 0, inputY: 0, - suppressedDirections: [], - lastTap: {}, lastRelease: {}, dashUntil: 0, dashVx: 0, runSince: 0, roundWins: 0, - attackKind: "", attackStartedAt: 0, - attackUntil: 0, attackHit: false, blocking: false, blockFlash: 0, - shieldLocked: false, shieldBrokenAt: 0, - windVx: 0, knockVx: 0, gunAmmo: 0, grenadeAmmo: 0, - gunAimX: -1, gunAimY: 0, stance: "NEUTRAL", - heldBall: -1, grabHeld: false, crouchBlend: 0, standingOn: -1, - partDamage: {}, removedParts: [], pogoHit: false, pogoDive: false, - pounding: false, poundFrom: 0, - commandStream: [], - botPresses: {}, - jumpLaunchAt: 0, jumpPoseUntil: 0, landPoseUntil: 0, - jumpHeld: false, hopUntil: 0, sinkUntil: 0, - crouchJump: false, attackMomentum: 1 }, -]; -const impacts = []; -const detachedParts = []; -const bullets = []; -const grenades = []; -// Pickups belong to a surface the player can actually see. When the optional -// ledge is gone they sit just above the floor instead of floating at its old -// invisible height. -const gunPickups = [ - { amount: 6, x: 6000, y: PLATFORM ? platformY - 70 : floorY - 35, z: 0 }, -]; -const grenadePickups = [ - { amount: 2, x: 6000, y: PLATFORM ? platformY - 70 : floorY - 35, z: 0 }, -]; -// Two trees grow out of the side walls, one per side, and they are the only -// thing in the round that gives a body back. A fighter who has been taken -// apart limb by limb can walk to the wall and be whole again — which is worth -// crossing the stage for, and worth the fifteen seconds it puts back on the -// clock. They take the whole round to ripen, so each one is picked at most -// once and the walk has to be timed. -const bodyTrees = [ - { x: worldLeft + wallThickness, y: floorY - 60, z: 0, growth: 0 }, - { x: worldRight - wallThickness, y: floorY - 60, z: 0, growth: 0 }, -]; -const treeRipenUs = 18000000; -const treeTimeBonusUs = 15000000; -const airParticles = []; -for (const pickup of [...gunPickups, ...grenadePickups]) { - pickup.active = false; - pickup.respawnAt = 0; -} -// Every kind carries a gravity factor so redressing the match ball in place -// can never leave a previous kind's float behind. -const ballKinds = [ - { type: "soccer", spawnOwner: 0, radius: 38, mass: .72, hitScale: 1.12, - bounce: .58, drag: .994, windFactor: .58, gravityFactor: 1 }, - { type: "basketball", spawnOwner: 1, radius: 42, mass: 1.08, hitScale: .86, - bounce: .76, drag: .989, windFactor: .34, gravityFactor: 1 }, - { type: "beach", spawnOwner: -1, radius: 46, mass: .34, hitScale: 1.25, - bounce: .82, drag: .998, windFactor: 1.35, gravityFactor: .62 }, -]; -let matchBallType = ballKinds[0].type; -// A match inflates exactly one ball and keeps it for every round. -const balls = [{ ...ballKinds[0], z: 0, vx: 0, vy: 0, rotation: 0, - x: players[0].spawnX, y: floorY - ballKinds[0].radius, - active: true, serveAt: 0, lastHitBy: 0, safeUntil: 0, safePlayers: 0, - heldBy: -1 }]; -// Version-one replay/spectator consumers still read the first ball by name. -const ball = balls[0]; -let ballEnabled = true; -let padSnapshots = [null, null]; -// The pad each fighter actually feels. A bot writes its synthesized presses -// here, so the HUD, the debug read-out, the replay and the physics all read -// bot input off the same wire as a hand on a controller. -let inputPads = [null, null]; -let startedAt = 0; -let roundStartedAt = 0; -let lastSimAt = 0; -let roundElapsedUs = 0; -let lastCountdownSecond = -1; -// The intro's own clock: which "3, 2, 1" second last rang, and whether the -// round-open accent has fired. -1 means the accent is still owed. -let lastIntroSecond = 0; -// The scored tail's clocks: the next heartbeat of the killcam dwell, and -// whether the result card has had its sting. -let resultPulseAt = 0; -let resultCardStung = false; -let roundOverAt = 0; -let roundResult = ""; -let matchOver = false; -let roundCause = ""; -let deathCinematic = null; -let impactHitboxesUntil = 0; -let nextPowerupAtUs = powerupIntervalUs; -let powerupSequence = 0; -let acFeed = {}; -// Pal select is retired: you are your own handle, so there is nobody to pick, -// and the dummy is already the default opponent. The whole two-step screen -// stays behind this one flag rather than being deleted, because the entry UI -// is still moving and this is the shape we may want to compare against. Flip -// it true and the wheel comes back exactly as it was. -const PAL_SELECT = false; -// The frame recorder remains intact for future review tooling, but replay is -// not part of the match flow. One flag restores the viewer, its controls and -// its offer together; production currently moves directly through results. -const INSTANT_REPLAY = false; -let selecting = false; -// Self-play is a harness mode: both fighters run the bot, no pad can enter or -// leave it, and rounds roll over on their own. -let selfPlay = false; -// The title is also the attract screen. Half of sessions get the quiet, -// cross-legged tableau; half get a standing face-off. Both remain still so -// the wordmark stays readable until the player asks the fight to begin. -// Hashing the already-created round name keeps the split stable for a visit -// without spending a second Math.random call (reel seeding relies on one). -let titleAttractMode = "still"; -const selectionReady = [false, false]; -const selectionPrevious = [[], []]; -let selectionStep = 0; -let selectionCursor = 0; -let windMph = 0; -let windDirection = 1; -let windAcceleration = 0; -let windTargetMph = 0; -let windTargetDirection = 1; -let nextWindChangeAt = 0; -let replay = null; -let replayLastCommand = [-1, -1]; -let replayNextCheckpointAt = 0; -let matchName = ""; -let seriesName = ""; -let previousRoundName = ""; -let roundReplayFrames = []; -let roundReplayLastAt = 0; -let instantReplay = null; -let replayOfferPrevious = []; -let shellMode = "MENU"; -let shellPrevious = []; -// Whether the stick was already leaned last frame, so a held lean reads as -// one gesture at the title rather than a machine-gun of entries. -let shellStickLive = false; -// Training teaches for two and a half minutes once the title lifts. This is -// session time, not round time: a knockout or a visit back to the title must -// not restart onboarding that the player has already outgrown. -let dummyGuideStartedAt = null; -// Unset is null, never -1: the console's monotonic clock can read negative -// (App.cpp overflows int64 converting QPC ticks past ~10 days of uptime), -// and a >= 0 sentinel then swallows every legitimate timestamp. -let titleTransitionAt = null; -// The wordmark is a toy: every letter of "oskiewar" and of "start" keeps its -// own swell and shudder, and the prompt keeps one shared bounce the letters -// take in turn. Paint owns these because they are pointer feel, not state. -const titleToys = []; -const promptToys = []; -let promptBounce = 0; -let titleToyAt = -1; -let navigationPrevious = [[], []]; -// Temporary live combat inspector. Keep this explicit so the production view -// can return to a clean presentation without changing combat geometry. -let debugHitboxes = false; -let nextInputDebugAt = 0; -let frameTelemetry = []; -let frameTelemetryFlushAt = 0; -let lastPaintAt = 0; -let displayFps = 0; -let liveSequence = 0; -let liveNextAt = 0; -let spectatorQr = null; -// Encoding a code costs about 59ms and is the whole 50-100ms tail in the -// frame histogram: it ran at every round reset, for a URL that changes once -// per round. Same URL, same code -- so keep the last one. -let spectatorQrUrl = ""; -let spectatorQrCache = null; -function spectatorCode(url) { - if (typeof qrcode !== "function") return null; - if (url === spectatorQrUrl && spectatorQrCache) return spectatorQrCache; - spectatorQrUrl = url; - spectatorQrCache = qrcode(url, { errorCorrectLevel: 1 }); - return spectatorQrCache; -} - -let roundViewer = null; -let roundViewerStop = null; -let roundViewerMode = ""; -let roundViewerStatus = "CONNECTING"; -let roundViewerDemo = null; -let roundViewerDemoStartedAt = 0; -let livePublishFailed = false; - -// Match names are public URLs and must not collide, so they take real -// entropy — but a recorded round has to reproduce, and the ball kind is -// hashed from the series name and carries radius and mass. So the entropy is -// drawn once per match, carried in the demo, and every name after it falls -// out deterministically. -let nameSeed = 1; -function seedNames(seed) { - nameSeed = (seed >>> 0) || 1; -} -function nameRandom() { - nameSeed = (nameSeed + 0x6d2b79f5) >>> 0; - let value = nameSeed; - value = Math.imul(value ^ (value >>> 15), value | 1); - value ^= value + Math.imul(value ^ (value >>> 7), value | 61); - return ((value ^ (value >>> 14)) >>> 0) / 4294967296; -} - -function pronounceableMatchName() { - const onsets = ["b", "d", "f", "g", "k", "l", "m", "n", "p", "r", - "s", "t", "v", "z", "ch", "sh", "th"]; - const consonants = "bdfgklmnprstvz"; - const vowels = "aeiou"; - const onset = onsets[Math.floor(nameRandom() * onsets.length)]; - const vowel = vowels[Math.floor(nameRandom() * vowels.length)]; - const middle = consonants[Math.floor(nameRandom() * consonants.length)]; - const ending = (vowels + "y")[Math.floor(nameRandom() * (vowels.length + 1))]; - return onset + vowel + middle + middle + ending + - Math.floor(nameRandom() * 1000); -} - -function demoTick(now) { - return replay ? Math.max(0, Math.round((now - replay.startedMonotonicUs) / - replayTickUs)) : 0; -} - -function trackMatchStarted() { - if (!roundIsTimed() || typeof analytics !== "function") return; - const device = typeof capabilities === "function" ? capabilities() : {}; - const platform = String(device.platform || "").toLowerCase(); - const surface = platform === "xbox-uwp" || platform === "xbox" - ? "xbox" - : platform === "macos" ? "macos" : "web"; - const family = String(device.inputFamily || "").toLowerCase(); - analytics("match_started", { - source_system: "browser", - surface, - input_family: ["gamepad", "keyboard", "xbox"].includes(family) - ? family - : "unknown", - opponent_type: selfPlay ? "self-play" - : players[1].bot ? "bot" - : players[1].npc ? "dummy" : "local-player", - }); -} - -function startReplay(now) { - const run = runtime(); - if (!roundIsTimed()) { - seriesName = ""; - matchName = ""; - previousRoundName = ""; - // Training has no series to seed from, so the chosen pal picks the ball. - matchBallType = seriesBallType(players[0].name); - replay = null; - spectatorQr = null; - return; - } - seedNames(Math.floor(Math.random() * 4294967296)); - const nameSeedUsed = nameSeed; - seriesName = pronounceableMatchName(); - matchBallType = seriesBallType(seriesName); - matchName = ""; - previousRoundName = ""; - replay = { - format: "ac.oskiedemo", version: 1, game: "oskiewar", - simulation: "oskiewar-physics-1", tickRate: 60, - matchId: "ow-" + seriesName, matchName: seriesName, - seriesId: "ow-" + seriesName, seriesName, roundIds: [], - startedAt: run.unixMs || 0, startedMonotonicUs: now, - nameSeed: nameSeedUsed, ballType: matchBallType, - fighters: players.map((player) => player.name), - commands: [], events: [], checkpoints: [], rounds: [], - }; - replayLastCommand = [-1, -1]; - replayNextCheckpointAt = now; - liveSequence = 0; - liveNextAt = now; - livePublishFailed = false; - spectatorQr = null; - trackMatchStarted(); -} - -function roundIsTimed() { - return !(players[1].npc && !players[1].bot); -} - -function spectatorState(now, nextRoundId = "") { - const introAge = now - roundStartedAt; - const phase = instantReplay ? "replay" : matchOver ? "match" - : roundResult ? "round" : introAge < introDurationUs ? "intro" : "fight"; - const timed = roundIsTimed(); - const remainingMs = roundResult ? 0 : timed ? Math.max(0, - Math.round((roundDurationUs - roundElapsedUs) / 1000)) : null; - const state = { - format: "ac.oskiewar.live", version: 1, seq: liveSequence++, - at: runtime().unixMs || 0, phase, - seriesId: "ow-" + seriesName, roundId: "ow-" + matchName, - previousRoundId: previousRoundName ? "ow-" + previousRoundName : "", - fighters: players.map((player) => ({ - name: player.name, color: player.color, x: player.x, y: player.y, - z: player.z, vx: player.vx, vy: player.vy, vz: player.vz, - facing: player.facing, alive: player.alive, - grounded: player.grounded, ducking: player.ducking, - sinking: now < player.sinkUntil, - blocking: player.blocking, score: player.score, - roundWins: player.roundWins, attack: player.attackKind || "", - removedParts: player.removedParts.slice(), - })), - ball: { active: ball.active, x: ball.x, y: ball.y, - z: ball.z, radius: ball.radius, type: ball.type, mass: ball.mass }, - balls: balls.map((item) => ({ active: item.active, x: item.x, - y: item.y, z: item.z, radius: item.radius, type: item.type, - mass: item.mass, heldBy: item.heldBy, - spawnOwner: item.spawnOwner })), - camera: { x: cameraCenter, y: cameraCenterY, width: cameraWidth, - position: { ...cameraDoll.position }, target: { ...cameraDoll.target }, - perspective: cameraDoll.perspective, fov: cameraDoll.fov, - roll: cameraDoll.roll }, - wind: { direction: windDirection, mph: windMph }, - round: { remainingMs, timed, result: roundResult || "", - cause: roundCause || "" }, - replayUrl: "/api/oskiewar-replays?id=ow-" + matchName, - }; - if (nextRoundId) state.nextRoundId = nextRoundId; - return state; -} - -function publishSpectator(now, { target = matchName, nextRoundId = "", - force = false } = {}) { - if (!roundIsTimed() || !target || livePublishFailed || - typeof publishLive !== "function" || - (!force && now < liveNextAt)) return; - liveNextAt = now + liveSnapshotIntervalUs; - try { - publishLive("ow-" + target, - JSON.stringify(spectatorState(now, nextRoundId))); - } catch (error) { - // A native host with an older room-ID contract must never take down play. - // Disable only spectator publishing until the next match/host upgrade. - livePublishFailed = true; - telemetry("OSKIEWAR_LIVE_DISABLED", String(error?.message || error)); - } -} - -function inputCommand(pad) { - const input = quantizedInput(pad); - let mask = 0; - if (input.horizontal < 0) mask |= 1; - if (input.horizontal > 0) mask |= 2; - if (input.vertical > 0) mask |= 4; - if (input.vertical < 0) mask |= 8; - for (let index = 4; index < replayButtons.length; index++) - if (pad.down.includes(replayButtons[index])) mask |= 1 << index; - return mask; -} - -function recordReplayCommands(now, inputs = padSnapshots) { - if (!replay) return; - for (let pad = 0; pad < players.length; pad++) { - const command = players[pad].npc && !players[pad].bot - ? 0 : inputCommand(inputs[pad]); - if (command !== replayLastCommand[pad]) { - replay.commands.push([demoTick(now), pad, command]); - replayLastCommand[pad] = command; - } - } -} - -function replayFlags(player) { - return (player.alive ? 1 : 0) | (player.grounded ? 2 : 0) | - (player.ducking ? 4 : 0) | (player.blocking ? 8 : 0) | - [...limbParts, "torso"].reduce((flags, part, index) => - flags | (hasPart(player, part) ? 0 : 1 << (index + 4)), 0); -} - -function recordReplayCheckpoint(now, force = false) { - if (!replay || (!force && now < replayNextCheckpointAt)) return; - replayNextCheckpointAt = now + replayCheckpointUs; - const values = [demoTick(now)]; - for (const player of players) values.push( - Math.round(player.x), Math.round(player.y), Math.round(player.z), - Math.round(player.vx), Math.round(player.vy), replayFlags(player), - player.score, player.roundWins); - values.push(Math.round(ball.x), Math.round(ball.y), Math.round(ball.z), - Math.round(ball.vx), Math.round(ball.vy), ball.active ? 1 : 0, - Math.round(cameraCenter), Math.round(cameraCenterY), Math.round(cameraWidth)); - replay.checkpoints.push(values); -} - -function makeRoundReplayFrame(now) { - const poseTime = (now - startedAt) / 1000000; - return { - players: players.map((player) => ({ - x: player.x, y: player.y, z: player.z, - vx: player.vx, vy: player.vy, vz: player.vz, - facing: player.facing, grounded: player.grounded, - ducking: player.ducking, alive: player.alive, - blocking: player.blocking, blockFlash: player.blockFlash, hit: player.hit, - attackKind: player.attackKind, - attackStartedOffset: player.attackStartedAt - now, - attackUntilOffset: player.attackUntil - now, - geometry: runnerWorldGeometry(player, poseTime), - })), - ball: { x: ball.x, y: ball.y, z: ball.z, vx: ball.vx, vy: ball.vy, - active: ball.active }, - balls: balls.map((item) => ({ x: item.x, y: item.y, z: item.z, - vx: item.vx, vy: item.vy, active: item.active })), - camera: { center: cameraCenter, centerY: cameraCenterY, width: cameraWidth }, - }; -} - -function captureRoundReplay(now, force = false) { - if (!INSTANT_REPLAY) return; - if (!force && now - roundReplayLastAt < instantReplayStepUs) return; - roundReplayLastAt = now; - roundReplayFrames.push(makeRoundReplayFrame(now)); - while (roundReplayFrames.length > instantReplayMaxFrames) - roundReplayFrames.shift(); -} - -const frameTelemetrySchema = ["us", "cameraX", "cameraY", "cameraWidth", - "dollX", "dollY", "dollZ", "targetX", "targetY", "targetZ", - "dollWidth", "roll", "p1x", "p1y", "p1z", "p1vx", "p1vy", - "p2x", "p2y", "p2z", "p2vx", "p2vy"]; - -function captureFrameTelemetry(now, force = false) { - if (selecting || shellMode === "MENU") return; - const round = (value) => Math.round(Number(value || 0) * 100) / 100; - frameTelemetry.push([ - Math.round(now - roundStartedAt), round(cameraCenter), round(cameraCenterY), - round(cameraWidth), round(cameraDoll.position.x), round(cameraDoll.position.y), - round(cameraDoll.position.z), round(cameraDoll.target.x), - round(cameraDoll.target.y), round(cameraDoll.target.z), - round(cameraDoll.width), round(cameraDoll.roll), - ...players.flatMap((player) => [round(player.x), round(player.y), - round(player.z), round(player.vx), round(player.vy)]), - ]); - if (!force && now < frameTelemetryFlushAt) return; - telemetry("FIGHT_TRACE", JSON.stringify({ - format: "ac.oskiewar.frames", version: 1, round: "ow-" + matchName, - schema: frameTelemetrySchema, frames: frameTelemetry, - })); - frameTelemetry = []; - frameTelemetryFlushAt = now + 1000000; -} - -function applyRoundReplayFrame(frame, now) { - for (let index = 0; index < players.length; index++) { - const player = players[index]; - const state = frame.players[index]; - for (const key of ["x", "y", "z", "vx", "vy", "vz", "facing", - "grounded", "ducking", "alive", "blocking", "blockFlash", "hit", - "attackKind"]) player[key] = state[key]; - player.attackStartedAt = now + state.attackStartedOffset; - player.attackUntil = now + state.attackUntilOffset; - player.replayGeometry = state.geometry; - } - const frameBalls = frame.balls || [frame.ball]; - for (let index = 0; index < balls.length; index++) { - const state = frameBalls[index]; - if (!state) continue; - for (const key of ["x", "y", "z", "vx", "vy", "active"]) - balls[index][key] = state[key]; - } - cameraCenter = frame.camera.center; - cameraCenterY = frame.camera.centerY; - cameraWidth = frame.camera.width; -} - -function finishInstantReplay(now) { - if (!instantReplay) return; - applyRoundReplayFrame(instantReplay.endFrame, now); - for (const player of players) delete player.replayGeometry; - instantReplay = null; - roundOverAt = now; - replayOfferPrevious = padSnapshots[0]?.down?.slice() || []; -} - -// A replay is worth watching at the speed the eye needs, not the speed the -// clock kept. Frames already record what a hit looks like, so the playhead can -// read its own footage and slow itself into the action rather than being told -// where the action is — which means every replay already in the store gets -// this without being re-recorded. -function frameAction(frame) { - let action = 0; - for (const player of frame.players) { - if (!player.alive) action = Math.max(action, 1); - if (player.hit) action = Math.max(action, 1); - if (player.blockFlash > 0) action = Math.max(action, .7 * player.blockFlash); - // A swing that has started but not yet landed is the part worth stretching. - if (player.attackKind && player.attackUntilOffset > 0) - action = Math.max(action, .35); - } - return action; -} - -// Look ahead, so the ramp is already down by the time the punch arrives. A -// slowdown that begins on the frame of impact has missed the impact. -function replayActionCurve(frames) { - const raw = frames.map(frameAction); - return raw.map((_, index) => { - let peak = 0; - for (let at = index; at < Math.min(raw.length, index + replayActionLead); at++) - peak = Math.max(peak, raw[at] * (1 - (at - index) / replayActionLead * .35)); - return peak; - }); -} - -// One step of the speed ramp, kept pure so it can be tested and so the demo -// viewer can share it when its playhead learns to accumulate. Easing rather -// than snapping — and capping how far the speed may travel per second of real -// time — is what keeps a dense run of hits reading as one long stretch rather -// than a stutter. -function replayRampStep(speed, action, elapsedSeconds) { - const target = lerp(1, replaySlowest, clamp(action, 0, 1)); - return speed + (target - speed) * - Math.min(1, Math.max(0, elapsedSeconds) * replayRampPerSecond); -} - -function startInstantReplay(now) { - if (!INSTANT_REPLAY) return false; - if (roundReplayFrames.length < 2) return false; - const frames = roundReplayFrames.slice(); - instantReplay = { frames, cursor: 0, lastAt: now, paused: false, - previous: padSnapshots[0]?.down?.slice() || [], - action: replayActionCurve(frames), speed: 1, - endFrame: frames[frames.length - 1] }; - impacts.length = 0; - detachedParts.length = 0; - applyRoundReplayFrame(frames[0], now); - telemetry("ROUND_REPLAY", "start frames=" + frames.length); - return true; -} - -function updateInstantReplay(now, dt) { - if (!instantReplay) return; - const down = padSnapshots[0]?.down || []; - const pressed = (button) => down.includes(button) && - !instantReplay.previous.includes(button); - if (pressed("B")) { - finishInstantReplay(now); - return; - } - if (pressed("A")) instantReplay.paused = !instantReplay.paused; - if (pressed("ArrowLeft") || pressed("ArrowRight")) { - instantReplay.paused = true; - instantReplay.cursor += pressed("ArrowLeft") ? -15 : 15; - } - const elapsed = Math.max(0, now - instantReplay.lastAt) / 1000000; - instantReplay.speed = replayRampStep(instantReplay.speed, - instantReplay.action?.[Math.floor(instantReplay.cursor)] || 0, elapsed); - if (!instantReplay.paused) - instantReplay.cursor += (now - instantReplay.lastAt) / - instantReplayStepUs * instantReplay.speed; - instantReplay.lastAt = now; - instantReplay.cursor = clamp(instantReplay.cursor, 0, - instantReplay.frames.length); - if (instantReplay.cursor >= instantReplay.frames.length) { - finishInstantReplay(now); - return; - } - applyRoundReplayFrame(instantReplay.frames[Math.floor(instantReplay.cursor)], now); - instantReplay.previous = down.slice(); - const target = { x: cameraCenter, y: cameraCenterY, z: 0 }; - cameraDoll.track({ target, - position: { x: cameraCenter - cameraWidth * .06, - y: cameraCenterY - cameraWidth * .04, z: -cameraWidth * 1.35 }, - width: cameraWidth, perspective: 0, fov: 55 }, dt, 18); -} - -function saveRoundReplay(now) { - if (!replay) return; - recordReplayCheckpoint(now, true); - const demo = JSON.parse(JSON.stringify(replay)); - demo.matchId = "ow-" + matchName; - demo.matchName = matchName; - demo.roundId = demo.matchId; - demo.roundName = matchName; - demo.roundIndex = Math.max(0, replay.roundIds.length - 1); - demo.previousRoundId = previousRoundName ? "ow-" + previousRoundName : ""; - demo.durationTicks = demoTick(now); - demo.winner = players[0].score === players[1].score ? null - : players[0].score > players[1].score ? players[0].name : players[1].name; - demo.finalRoundWins = players.map((player) => player.roundWins); - delete demo.startedMonotonicUs; - const payload = JSON.stringify(demo); - if (payload.length <= 524288 && typeof saveReplay === "function") { - const upload = saveReplay(payload); - // Only promise-returning hosts can prove the upload completed. The web - // host does; older native hosts keep saving silently until they adopt the - // same acknowledgement contract. - if (upload && typeof upload.then === "function") - upload.then((saved) => { - if (saved !== true) return; - playDrum("modem", .72, 0); - telemetry("REPLAY", "uploaded " + demo.roundId); - }).catch((error) => telemetry("REPLAY", "upload-error " + error.message)); - telemetry("REPLAY", "queued " + demo.roundId + " bytes=" + payload.length); - } else telemetry("REPLAY", "not-saved bytes=" + payload.length); -} - -function finishReplay() { - replay = null; -} - -function emitSignal(event, player = -1, value = 0, value2 = 0) { - if (replay) replay.events.push([demoTick(runtime().monotonicUs), event, - player, Math.round(value * 1000) / 1000, Math.round(value2 * 1000) / 1000]); - if (typeof gameSignal === "function") gameSignal(event, player, value, value2); -} - -// Revision 37 exposes native bell/woosh voices in the mixer but its QuickJS -// allowlist predates those names. Fall back without stopping the match; newer -// hosts and the browser still receive the authored voice unchanged. -function playDrum(name, velocity = 1, pan = 0) { - if (typeof drum !== "function") return; - try { - drum(name, velocity, pan); - } catch (error) { - if (name !== "bell" && name !== "whoosh") throw error; - if (name === "bell" && typeof synth === "function") { - try { synth(880, .12); } catch (_) {} - } - try { - drum(name === "bell" ? "hat" : "block", velocity * .65, pan); - } catch (_) {} - } -} - -let clientError = ""; -let clientErrorDetail = null; -let clientErrorAt = -1; -let clientErrorQr = null; -let clientErrorDumpUrl = ""; - -function clientStateDump() { - try { - const run = runtime(); - return { - build: buildTimestamp, - shell: shellMode, - round: { - id: matchName || "local", - result: roundResult || "active", - elapsedMs: Math.round(roundElapsedUs / 1000), - }, - runtime: { - sim: Number(run?.simCount || 0), - paint: Number(run?.paintCount || 0), - unixMs: Number(run?.unixMs || 0), - }, - camera: { - x: Math.round(cameraCenter), y: Math.round(cameraCenterY), - width: Math.round(cameraWidth), aspect: Math.round(cameraAspect * 1000) / 1000, - }, - players: players.map((player) => ({ - handle: player.name, stance: player.stance, alive: player.alive, - x: Math.round(player.x), y: Math.round(player.y), z: Math.round(player.z), - vx: Math.round(player.vx), vy: Math.round(player.vy), - input: [player.inputX, player.inputY], - removed: player.removedParts?.slice() || [], - })), - balls: balls.filter((item) => item.active).map((item) => ({ - type: item.type, x: Math.round(item.x), y: Math.round(item.y), - vx: Math.round(item.vx), vy: Math.round(item.vy), heldBy: item.heldBy, - })), - }; - } catch (error) { - return { dumpError: String(error?.message || error || "state unavailable") }; - } -} - -function errorNowUs() { - try { - const value = Number(runtime()?.monotonicUs); - if (Number.isFinite(value)) return value; - } catch (_) {} - return 0; -} - -// btoa only speaks latin-1, so the dump flattens to ASCII before encoding. -function base64Url(text) { - if (typeof btoa !== "function") return ""; - return btoa(String(text).replace(/[^\x20-\x7e]/g, "?")) - .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -} - -// The QR carries the entire dump in its own URL, so a crash stays shareable -// even when the console never managed to post its report. Fat fields drop -// away until the payload fits a code that is still scannable off a TV. -function buildClientErrorDump(detail) { - clientErrorQr = null; - clientErrorDumpUrl = ""; - const stack = String(detail.stack || "").split(/\r?\n/).slice(0, 4) - .map((line) => line.trim()).filter(Boolean).join(" | ").slice(0, 240); - for (const trace of [stack, ""]) { - const url = errorDumpBase + base64Url(JSON.stringify({ - v: 1, p: detail.phase, n: detail.name, m: detail.message, - src: detail.source, k: trace || undefined, s: detail.state, - })); - if (url.length <= 2048 && typeof qrcode === "function") { - try { - clientErrorQr = qrcode(url, { errorCorrectLevel: 1 }); - clientErrorDumpUrl = url; - return; - } catch (_) {} - } - } -} - -function errorRestartSeconds() { - if (clientErrorAt < 0) return 0; - return Math.max(0, - Math.ceil((errorRestartUs - (errorNowUs() - clientErrorAt)) / 1000000)); -} - -// A crashed console should not sit on a dead screen forever. The error holds -// long enough to read and scan, then the piece boots itself again. -function restartAfterClientError() { - if (clientErrorAt < 0 || errorNowUs() - clientErrorAt < errorRestartUs) return; - clientError = ""; - clientErrorDetail = null; - clientErrorAt = -1; - clientErrorQr = null; - clientErrorDumpUrl = ""; - impacts.length = 0; - detachedParts.length = 0; - bullets.length = 0; - grenades.length = 0; - try { telemetry("SHELL", "error->restart"); } catch (_) {} - boot(); -} - -function errorSource(stack) { - const match = String(stack || "").match( - /(?:\(|\s|^)((?:[a-z]+:\/\/)?[^\s()]+):(\d+):(\d+)\)?/i); - return match ? { file: match[1], line: Number(match[2]), column: Number(match[3]) } - : null; -} - -function captureClientError(phase, error) { - if (clientErrorDetail) return; - const stack = error?.stack ? String(error.stack) : ""; - const message = error?.message ? String(error.message) - : String(error || "unknown error"); - const name = String(error?.name || "Error"); - const state = clientStateDump(); - clientErrorDetail = { - phase: String(phase || "runtime"), name, message, stack, - source: errorSource(stack), state, - reportStatus: "queued for server", - }; - const detail = stack || (name + ": " + message); - clientError = (clientErrorDetail.phase + ": " + detail) - .replace(/[^\x20-\x7e]+/g, " ").replace(/\s+/g, " ").trim(); - clientErrorAt = errorNowUs(); - try { buildClientErrorDump(clientErrorDetail); } catch (_) { - clientErrorQr = null; - clientErrorDumpUrl = ""; - } - try { - telemetry("CLIENT_ERROR", JSON.stringify({ - phase: clientErrorDetail.phase, name, message, stack, - source: clientErrorDetail.source, state, - })); - } catch (_) { - clientErrorDetail.reportStatus = "local report only"; - } -} - -function clientErrorLines(text, limit = 58) { - const words = String(text).split(" "); - const lines = []; - let line = ""; - for (let word of words) { - if (word.length > limit) { - if (line) { lines.push(line); line = ""; } - while (word.length > limit) { - lines.push(word.slice(0, limit)); - word = word.slice(limit); - } - if (!word) continue; - } - if (!line) line = word; - else if (line.length + word.length + 1 <= limit) line += " " + word; - else { lines.push(line); line = word; } - } - if (line) lines.push(line); - return lines.slice(0, 9); -} - -function errorTypeWrite(text, x, y, size, ...color) { - try { - typeWrite(text, x, y, size, ...color); - } catch (_) { - // Error reporting must survive a font renderer failure of its own. - try { systemWrite(String(text).toLowerCase(), x, y, size, ...color); } - catch (_) {} - } -} - -// Diagnostic lines are dense, so every token gets its own ink: handles read as -// people, numbers as measurements, units as scale, and the rest as labels. -const dumpUnits = new Set(["px", "ms", "us", "fps", "vx", "vy", "vz", "in", - "stk", "pos", "vel", "t", "pts"]); - -function dumpTokens(text) { - return String(text).match(/@[a-z0-9_]+|[-+]?\d*\.?\d+|[a-z]+|\s+|./gi) || []; -} - -function dumpTokenInk(token) { - if (token[0] === "@") return [255, 126, 205]; - if (/^[-+]?[\d.]+$/.test(token)) return [116, 255, 184]; - if (/^[a-z]+$/i.test(token)) return dumpUnits.has(token.toLowerCase()) - ? [255, 205, 74] : [188, 204, 230]; - return [112, 234, 255]; -} - -function writeTokens(write, text, x, y, size) { - let cursor = x; - for (const token of dumpTokens(text)) { - if (token.trim()) write(token, cursor, y, size, ...dumpTokenInk(token)); - cursor += handleWidth(token, size); - } -} - -const writeDumpLine = (text, x, y, size) => - writeTokens(errorTypeWrite, text, x, y, size); - -// The HUD sits over sky and ground, so each token carries its own contrast -// shadow the way a handle glyph does. -function writeHudLine(text, x, y, size) { - writeTokens((token, tx, ty, ts, ...color) => - typeWrite(token, tx + 3, ty + 4, ts, ...runShadow(color)), - text, x, y, size); - writeTokens(typeWrite, text, x, y, size); -} - -function errorReportStatus() { - try { - const status = String(runtime()?.clientErrorReportStatus || "").trim(); - if (status) return status; - } catch (_) {} - return clientErrorDetail?.reportStatus || "queued for server"; -} - -function stateDumpRows(state) { - if (!state) return [{ icon: "mode", text: "state unavailable" }]; - if (state.dumpError) - return [{ icon: "mode", text: "state unavailable: " + state.dumpError }]; - const playerRow = (player, index) => ({ icon: "player", - text: "p" + (index + 1) + " " + player.handle + " " + - player.stance.toLowerCase() + " pos " + player.x + "," + player.y + - "," + player.z + " vel " + player.vx + "," + player.vy + - (player.removed?.length ? " lost " + player.removed.join(",") : "") }); - return [ - { icon: "build", text: "build " + state.build }, - { icon: "mode", text: "mode " + state.shell.toLowerCase() + - " round " + state.round.id + " " + state.round.result.toLowerCase() + - " " + state.round.elapsedMs + "ms" }, - { icon: "camera", text: "camera " + state.camera.x + "," + state.camera.y + - " width " + state.camera.width + " aspect " + state.camera.aspect }, - ...(state.players || []).map(playerRow), - { icon: "ball", text: "balls " + (state.balls || []).map((ball) => - ball.type + "@" + ball.x + "," + ball.y + " v" + ball.vx + "," + ball.vy) - .join(" ") }, - ]; -} - -const dumpIconInk = { build: [116, 128, 156], mode: [255, 205, 74], - camera: [112, 214, 255], player: [206, 166, 255], ball: [116, 255, 184] }; - -function drawDumpIcon(kind, x, y, size) { - const ink = dumpIconInk[kind] || [112, 234, 255]; - const unit = Math.max(2, Math.round(size / 9)); - try { - if (kind === "build") - for (let row = 0; row < 3; row++) - box(x, y + row * unit * 3, size - row * unit * 2, unit * 2, ...ink); - else if (kind === "camera") { - strokeBox(x, y + unit, size, size - unit * 2, unit, ink); - box(x + unit * 3, y + unit * 3, size - unit * 6, size - unit * 6, ...ink); - } else if (kind === "player") { - box(x + unit * 2, y, size - unit * 4, unit * 3, ...ink); - box(x, y + unit * 4, size, size - unit * 4, ...ink); - } else if (kind === "ball") { - box(x + unit * 2, y, size - unit * 4, size, ...ink); - box(x, y + unit * 2, size, size - unit * 4, ...ink); - } else strokeBox(x, y, size, size, unit, ink); - } catch (_) {} -} - -// Sized for a phone pointed at a television, so modules stay chunky and the -// state dump wraps narrower to keep clear of the code. -function errorQrGeometry(width, height) { - if (!clientErrorQr || typeof clientErrorQr.getModuleCount !== "function") - return null; - const count = clientErrorQr.getModuleCount(); - const quiet = 2; - const cell = Math.max(2, - Math.floor(Math.min(width * .3, height * .52) / (count + quiet * 2))); - const size = (count + quiet * 2) * cell; - return { count, quiet, cell, size, - left: width - 92 - size, top: height - 96 - size }; -} - -// Drawn with raw boxes rather than the projected screenRect the HUD uses — -// the triangle pipeline is exactly what may have just died. Dark modules -// coalesce into horizontal runs so a full code stays a few hundred draws. -function drawErrorQr(qr) { - if (!qr) return; - box(qr.left, qr.top, qr.size, qr.size, 250, 250, 247); - for (let row = 0; row < qr.count; row++) { - let run = 0; - for (let column = 0; column <= qr.count; column++) { - if (column < qr.count && clientErrorQr.isDark(row, column)) { run++; continue; } - if (run) box(qr.left + (column - run + qr.quiet) * qr.cell, - qr.top + (row + qr.quiet) * qr.cell, run * qr.cell, qr.cell, 7, 8, 14); - run = 0; - } - } -} - -// Same two-digit face as the round timer, reddening as the restart lands. -function drawErrorCountdown(width) { - const remaining = errorRestartSeconds(); - const text = String(Math.min(99, remaining)).padStart(2, "0"); - const danger = remaining <= 10; - const shake = danger - ? Math.sin(errorNowUs() / 1000000 * 35) * (11 - remaining) * .45 : 0; - let textWidth = 64; - try { textWidth = handleWidth(text, 52); } catch (_) {} - errorTypeWrite(text, width - 92 - textWidth + shake, 82, 52, - ...(danger ? [255, 92, 116] : [255, 205, 74])); -} - -function drawClientError() { - let width = 1920; - let height = 1080; - try { - const view = typeof gameView === "function" ? gameView() : null; - if (view && Number.isFinite(view.width)) width = view.width; - if (view && Number.isFinite(view.height)) height = view.height; - } catch (_) {} - const detail = clientErrorDetail || { - phase: "runtime", name: "Error", message: clientError, - stack: "", source: null, state: null, - }; - const qr = errorQrGeometry(width, height); - wipe(7, 9, 18); - box(48, 48, width - 96, height - 96, 30, 14, 27); - errorTypeWrite("the game needs a moment", 92, 82, 52, 255, 92, 116); - drawErrorCountdown(width); - errorTypeWrite("restarting safely", 94, 158, 31, 255, 205, 74); - const messageLines = clientErrorLines(detail.message, 66).slice(0, 2); - for (let index = 0; index < messageLines.length; index++) - errorTypeWrite(messageLines[index], 94, 226 + index * 42, - 31, 248, 244, 255); - let cursorY = 226 + messageLines.length * 42 + 28; - if (detail.source) { - errorTypeWrite("source", 94, cursorY, 29, 112, 234, 255); - errorTypeWrite(detail.source.file, 214, cursorY, 29, 190, 216, 255); - errorTypeWrite("line " + detail.source.line + " column " + detail.source.column, - Math.min(width - 430, 214 + detail.source.file.length * 17), cursorY, - 29, 116, 255, 184); - cursorY += 48; - } - errorTypeWrite("state dump", 94, cursorY, 29, 116, 255, 184); - cursorY += 42; - for (const row of stateDumpRows(detail.state).slice(0, 6)) { - let leading = true; - for (const wrapped of clientErrorLines(row.text, qr ? 68 : 78).slice(0, 2)) { - if (cursorY > height - 196) break; - if (leading) drawDumpIcon(row.icon, 94, cursorY + 4, 22); - writeDumpLine(wrapped, 132, cursorY, 27); - cursorY += 36; - leading = false; - } - } - drawErrorQr(qr); - if (qr) errorTypeWrite("scan to share this dump", - qr.left, qr.top - 42, 27, 116, 255, 184); - const report = errorReportStatus(); - const posted = report.startsWith("posted"); - errorTypeWrite(report, 94, height - 150, 30, - ...(posted ? [116, 255, 184] : [255, 205, 74])); - errorTypeWrite("relaunch or deploy an update", 94, height - 104, - 30, 190, 202, 230); -} - -function drawClientErrorFallback() { - try { wipe(7, 9, 18); } catch (_) {} - try { systemWrite("aesthetic.computer error", 72, 72, 48, 255, 92, 116); } catch (_) {} - try { systemWrite(errorReportStatus(), 72, 146, 30, 255, 205, 74); } catch (_) {} - try { - systemWrite("restart in " + errorRestartSeconds() + "s", 72, 200, 30, - 255, 205, 74); - } catch (_) {} -} - -function fighterProfile(handle) { - const live = Array.isArray(acFeed.fighters) - ? acFeed.fighters.find((profile) => profile.handle.toUpperCase() === handle.toUpperCase()) - : null; - const fallback = handle === "DUMMY" ? npcFighter - : handle === "SPIDERDUMMY" ? spiderDummyFighter - : handle === "BOT" ? botFighter - : selfPlayFighters.find((profile) => profile.handle === handle) || - fighterRoster.find((profile) => profile.handle === handle); - return { - mood: live?.mood || (handle === "@JEFFREY" && acFeed.moodHandle === "@jeffrey" - ? acFeed.mood : "") || fallback?.mood || "", - lastChat: live?.lastChat || fallback?.lastChat || "", - colors: live?.colors?.length - ? live.colors.map((color) => [color.r, color.g, color.b]) - : fallback?.colors || [], - }; -} - -function applyRoster(player, index) { - if (player.npc) { - const fighter = selfPlay ? selfPlayFighters[player.pad] - : player.bot ? botFighter - : player.spiderDummy ? spiderDummyFighter : npcFighter; - player.rosterIndex = -1; - player.name = fighter.handle; - player.color = fighter.color.slice(); - player.handleColors = fighter.colors; - return; - } - const rosterIndex = (index + fighterRoster.length) % fighterRoster.length; - const fighter = fighterRoster[rosterIndex]; - const profile = fighterProfile(fighter.handle); - player.rosterIndex = rosterIndex; - player.name = fighter.handle; - player.color = fighter.color.slice(); - player.handleColors = profile.colors; -} - -// The one door into a fight, and the seam auth will attach to. Dummy is free -// and anonymous — it is the front door and must never ask for anything. Bot -// and ppl are what a handle buys, because they persist: a series, a published -// match, a replay, a ranking. Sign-in belongs on this call, not in front of -// the game. Nothing opens those doors yet, so only training reaches here. -function startFightAgainst(kind, now) { - const opponent = players[1]; - opponent.npc = kind === "dummy" || kind === "spiderdummy" || kind === "bot"; - opponent.bot = kind === "bot"; - opponent.spiderDummy = kind === "spiderdummy"; - // Pad two is @OSKIE until ppl arrives carrying a handle of its own. - applyRoster(opponent, opponent.npc ? -1 : 2); - selecting = false; - shellMode = "GAME"; - titleTransitionAt = null; - startReplay(now); - resetRound(now, true); - emitSignal("fighters", -1, players[0].rosterIndex, players[1].rosterIndex); -} - -// Whatever you land on is already a live training fight; the wordmark simply -// floats over it. The intro countdown is spent before the first frame so -// somebody arriving from a QR code is moving, not watching a number. -function beginTraining(now) { - startFightAgainst("dummy", now); - shellMode = "MENU"; - roundStartedAt = now - introDurationUs; - const forcedAttract = globalThis.__oskiewarAttractVariant; - titleAttractMode = forcedAttract === "still" || forcedAttract === "action" - ? forcedAttract : hashUnit(matchName) < .5 ? "still" : "action"; - if (titleAttractMode === "action") { - for (const player of players) { - player.npc = true; - player.bot = true; - player.spiderDummy = false; - applyRoster(player, -1); - } - } -} - -// Deprecated with PAL_SELECT — see the flag. -function beginSelect(now) { - selecting = true; - selectionStep = 0; - selectionCursor = Math.max(0, players[0].rosterIndex); - selectionReady[0] = false; - selectionReady[1] = false; - selectionPrevious[0] = padSnapshots[0]?.down?.slice() || []; - selectionPrevious[1] = padSnapshots[1]?.down?.slice() || []; - roundResult = ""; - roundCause = ""; - deathCinematic = null; - matchOver = false; - roundElapsedUs = 0; - lastCountdownSecond = -1; - lastIntroSecond = 0; - resultPulseAt = 0; - resultCardStung = false; - roundStartedAt = now; - for (const player of players) { - player.roundWins = 0; - player.score = 0; - player.alive = true; - } -} - -function returnToTitle(now, reason = "back") { - finishReplay(); - beginTraining(now); - shellPrevious = padSnapshots[0]?.down?.slice() || []; - selectionPrevious[0] = shellPrevious.slice(); - selectionPrevious[1] = padSnapshots[1]?.down?.slice() || []; - spectatorQr = typeof qrcode === "function" - ? spectatorCode("https://oskiewar.com") : null; - playDrum("block", .8, 0); - telemetry("SHELL", "game->title " + reason + " " + now); -} - -// A mechanical test drives both fighters with the same bot and says so on the -// nameplates, instead of flying a handle nobody is holding. Entered through -// `__oskiewarSelfPlay` before boot or by calling this — never from a button, -// so normal play cannot fall into it. -function startSelfPlay(now) { - selfPlay = true; - shellMode = "GAME"; - selecting = false; - titleTransitionAt = null; - for (const player of players) { - player.npc = true; - player.bot = true; - player.spiderDummy = false; - player.rosterIndex = -1; - } - startReplay(now); - resetRound(now, true); -} - -function consumeSystemButtons(now) { - let pressed = false; - for (let index = 0; index < padSnapshots.length; index++) { - const down = padSnapshots[index]?.down || []; - const previous = navigationPrevious[index]; - if (down.includes("View") && !previous.includes("View")) { - debugHitboxes = !debugHitboxes; - telemetry("FIGHT_DEBUG", debugHitboxes ? "on" : "off"); - } - if (down.includes("Menu") && !previous.includes("Menu")) pressed = true; - navigationPrevious[index] = down.slice(); - } - // Self-play has no title to return to — a stray Menu press would strand the - // loop on a screen nobody is there to start. - if (!pressed || selfPlay || shellMode !== "GAME" || selecting) return false; - returnToTitle(now, "menu"); - return true; -} - -// Start lifts the wordmark off a fight that is already running underneath. -function enterGame(now) { - if (titleAttractMode === "action") { - selfPlay = false; - players[0].npc = false; - players[0].bot = false; - players[0].spiderDummy = false; - applyRoster(players[0], Math.max(0, players[0].rosterIndex)); - } - shellMode = "GAME"; - if (dummyGuideStartedAt === null && players[1].npc && !players[1].bot) - dummyGuideStartedAt = now; - shellPrevious = padSnapshots[0]?.down?.slice() || []; - if (PAL_SELECT) beginSelect(now); -} - -function updateShell(now) { - const pad = padSnapshots[0] || {}; - // View and Menu are the system's buttons — View toggles the debug HUD and - // must not double as "start the game". Everything else, and a real lean on - // the analog stick, says the player wants in. - const down = (pad.down || []) - .filter((button) => button !== "View" && button !== "Menu"); - const stickLive = Math.abs(pad.leftX || 0) > .5 || Math.abs(pad.leftY || 0) > .5; - if (titleTransitionAt !== null) { - if (now - titleTransitionAt >= 700000) { - titleTransitionAt = null; - enterGame(now); - } - shellPrevious = down.slice(); - shellStickLive = stickLive; - return; - } - if (down.some((button) => !shellPrevious.includes(button)) || - (stickLive && !shellStickLive)) { - playDrum("hat", .55, 0); - if (typeof titleBeep === "function") titleBeep(); - if (typeof titleVoice === "function") titleVoice(); - emitSignal("select", -1, 1, 0); - titleTransitionAt = now; - } - shellPrevious = down.slice(); - shellStickLive = stickLive; -} - -// Everything from here through `updateSelect` is the deprecated pal select, -// held behind PAL_SELECT — see the flag. -function selectionOptions() { - if (selectionStep === 0) return fighterRoster.map((fighter, rosterIndex) => ({ - kind: "pal", fighter, rosterIndex, - })); - return [ - { kind: "bot", fighter: botFighter, rosterIndex: -1 }, - { kind: "dummy", fighter: npcFighter, rosterIndex: -1 }, - { kind: "spiderdummy", fighter: spiderDummyFighter, rosterIndex: -1 }, - { kind: "people", fighter: peopleFighter, rosterIndex: -1, disabled: true }, - ]; -} - -function startSelectedRound(now) { - selectionReady[0] = true; - selectionReady[1] = true; - startFightAgainst( - players[1].bot ? "bot" : players[1].spiderDummy ? "spiderdummy" - : players[1].npc ? "dummy" : "ppl", now); -} - -function chooseSelection(index, now) { - const option = selectionOptions()[index]; - if (!option) return; - if (option.disabled) { - playDrum("block", .45, 0); - return; - } - if (selectionStep === 0) { - applyRoster(players[0], option.rosterIndex); - selectionReady[0] = true; - selectionStep = 1; - // The dummy is where a fight should start: it stands still and lets you - // learn the buttons. Picking the bot is a decision, not a default. - selectionCursor = Math.max(0, - selectionOptions().findIndex((option) => option.kind === "dummy")); - playDrum("hat", .9, -.55); - emitSignal("select", 0, option.rosterIndex, 0); - return; - } - const opponent = players[1]; - opponent.npc = option.kind === "dummy" || option.kind === "spiderdummy" || - option.kind === "bot"; - opponent.bot = option.kind === "bot"; - opponent.spiderDummy = option.kind === "spiderdummy"; - applyRoster(opponent, option.rosterIndex); - playDrum("clap", 1, .55); - emitSignal("select", 1, option.rosterIndex, opponent.bot ? 2 : opponent.npc ? 1 : 0); - startSelectedRound(now); -} - -function selectionBack(now) { - if (selectionStep === 1) { - selectionStep = 0; - selectionCursor = Math.max(0, players[0].rosterIndex); - selectionReady[0] = false; - playDrum("block", .7, 0); - return; - } - returnToTitle(now, "back"); -} - -// One pal at a time: the cursor holds the middle of the screen and its two -// neighbours recede to either side, so the roster reads as a wheel you turn -// rather than a board you scan. -function selectionTouchLayout() { - const count = selectionOptions().length; - const compact = compactLayout(); - const centerX = viewCenterX(); - const cardWidth = compact ? Math.min(280, viewWidth() * .44) : 680; - const cardHeight = compact ? 360 : 720; - const top = compact ? 176 : 190; - const sideScale = compact ? .5 : .66; - const reach = cardWidth * (1 + sideScale) / 2 + (compact ? 6 : 26); - const slots = count > 2 ? [-1, 0, 1] : count > 1 ? [0, 1] : [0]; - return { - back: compact - ? { x: 24, y: 126, width: 112, height: 50 } - : { x: viewOffsetX() + 90, y: 112, width: 170, height: 70 }, - options: slots.map((slot) => { - const scale = slot === 0 ? 1 : sideScale; - const width = cardWidth * scale; - const height = cardHeight * scale; - return { index: (selectionCursor + slot + count) % count, slot, - x: centerX + slot * reach - width / 2, - y: top + (cardHeight - height) / 2, width, height }; - }), - }; -} - -const pointInRect = (point, rect) => rect && point.x >= rect.x && - point.x <= rect.x + rect.width && point.y >= rect.y && - point.y <= rect.y + rect.height; - -function selectionHover(layout = selectionTouchLayout()) { - const pointer = globalThis.__oskiewarTouch?.pointer; - if (!pointer?.active || !Number.isFinite(pointer.x) || - !Number.isFinite(pointer.y)) return null; - if (pointInRect(pointer, layout.back)) return { back: true }; - const option = layout.options.find((rect) => pointInRect(pointer, rect)); - return option && !selectionOptions()[option.index]?.disabled - ? { option: option.index } : null; -} - -function consumeSelectTouches(now) { - const queue = globalThis.__oskiewarTouch?.taps; - if (!Array.isArray(queue) || !queue.length) return; - const touches = queue.splice(0); - const layout = selectionTouchLayout(); - const options = selectionOptions(); - for (const point of touches) { - if (pointInRect(point, layout.back)) selectionBack(now); - else { - const rect = layout.options.find((option) => pointInRect(point, option)); - // A receding neighbour turns the wheel; the focused card commits. A - // disabled card duds either way. - if (rect && (rect.slot === 0 || options[rect.index]?.disabled)) - chooseSelection(rect.index, now); - else if (rect) { - selectionCursor = rect.index; - playDrum("hat", .55, 0); - } - } - if (!selecting || shellMode === "MENU") break; - } -} - -function updateSelect(now) { - consumeSelectTouches(now); - if (!selecting || shellMode === "MENU") return; - const down = padSnapshots[0]?.down || []; - const previous = selectionPrevious[0]; - const pressed = (button) => down.includes(button) && !previous.includes(button); - const optionCount = selectionOptions().length; - let movement = 0; - if (pressed("ArrowLeft") || pressed("ArrowUp")) movement = -1; - else if (pressed("ArrowRight") || pressed("ArrowDown")) movement = 1; - if (movement) { - let next = selectionCursor; - for (let index = 0; index < optionCount; index++) { - next = (next + movement + optionCount) % optionCount; - if (!selectionOptions()[next]?.disabled) break; - } - if (next !== selectionCursor) { - selectionCursor = next; - playDrum("hat", .55, 0); - } - } - if (pressed("B") || pressed("Menu")) selectionBack(now); - else if (pressed("A")) chooseSelection(selectionCursor, now); - selectionPrevious[0] = down.slice(); - selectionPrevious[1] = padSnapshots[1]?.down?.slice() || []; -} - -function randomWindMph() { - return 0; -} - -function rollWind(now = runtime().monotonicUs, immediate = true) { - windTargetMph = 0; - windTargetDirection = 1; - nextWindChangeAt = Infinity; - windMph = 0; - windDirection = 1; - windAcceleration = 0; - emitSignal("wind", -1, windTargetDirection, windTargetMph); -} - -function updateWind(dt, now) { - windMph = 0; - windDirection = 1; - windAcceleration = 0; -} - -// The match ball is drawn from the series identity. That identity is itself -// seeded once per match and recorded, so re-running a demo's inputs inflates -// the same ball -- the name this hashes used to come straight from -// Math.random, which silently made ball radius and mass unreproducible. -function seriesBallType(name) { - const roll = hashUnit("ball:" + (name || "oskiewar")); - return ballKinds[Math.min(ballKinds.length - 1, - Math.floor(roll * ballKinds.length))].type; -} - -function resetBalls(now) { - Object.assign(balls[0], - ballKinds.find((kind) => kind.type === matchBallType) || ballKinds[0]); - for (const item of balls) { - const owner = item.spawnOwner >= 0 ? players[item.spawnOwner] : null; - item.x = owner ? owner.spawnX + owner.facing * 180 : platformRight + 820; - item.y = owner ? floorY - item.radius : floorY - 920; - // The beach ball begins airborne beyond the platform edge, so its first - // landing is in the open arena rather than on the center platform. - item.z = owner ? owner.z : 0; - item.vx = 0; - item.vy = 0; - item.rotation = 0; - item.heldBy = -1; - item.active = ballEnabled; - item.serveAt = now + introDurationUs + 150000; - item.lastHitBy = owner ? owner.pad : -1; - item.safeUntil = item.serveAt; - item.safePlayers = owner ? 1 << owner.pad : 0; - emitSignal("ballserve", owner ? owner.pad : -1, - owner ? owner.facing : 0, windMph); - } -} - -const buttonLabel = (button) => ({ - ArrowUp: "UP", ArrowDown: "DOWN", ArrowLeft: "LEFT", ArrowRight: "RIGHT", - LeftShoulder: "LB", RightShoulder: "RB", LeftStick: "LEFT STICK", - RightStick: "RIGHT STICK", View: "VIEW", Menu: "MENU", - LeftTrigger: "LT", RightTrigger: "RT", -}[button] || String(button).toUpperCase()); - -function roundDemoState(demo, now) { - const checkpoints = demo?.checkpoints || []; - if (!checkpoints.length) return null; - const roundIndex = Number.isInteger(demo.roundIndex) ? demo.roundIndex - : Math.max(0, (demo.rounds?.length || 1) - 1); - const startTick = demo.rounds?.[roundIndex]?.[0] || 0; - const finalCheckpoint = checkpoints[checkpoints.length - 1]; - const endTick = Math.max(startTick + 1, demo.durationTicks || finalCheckpoint[0]); - const elapsed = Math.floor((now - roundViewerDemoStartedAt) / replayTickUs); - const tick = startTick + elapsed % (endTick - startTick + 1); - let before = checkpoints[0], after = finalCheckpoint; - for (const row of checkpoints) { - if (row[0] <= tick) before = row; - if (row[0] >= tick) { after = row; break; } - } - const amount = clamp((tick - before[0]) / Math.max(1, after[0] - before[0]), 0, 1); - const value = (index) => lerp(before[index], after[index], amount); - const recentAttack = (pad) => { - let kind = ""; - for (const event of demo.events || []) { - if (event[0] > tick || event[0] < tick - 18 || event[2] !== pad) continue; - if (meleeSpecs[event[1].toUpperCase()]) kind = event[1].toUpperCase(); - } - return kind; - }; - const fighters = [0, 1].map((pad) => { - const offset = 1 + pad * 8; - const flags = before[offset + 5]; - const name = demo.fighters?.[pad] || `P${pad + 1}`; - const profile = name === "DUMMY" ? npcFighter : name === "BOT" ? botFighter - : fighterRoster.find((fighter) => fighter.handle === name); - const vx = value(offset + 3); - return { name, color: profile?.color || players[pad].color, - x: value(offset), y: value(offset + 1), z: value(offset + 2), - vx, vy: value(offset + 4), vz: 0, - facing: vx ? Math.sign(vx) : pad ? -1 : 1, - alive: Boolean(flags & 1), grounded: Boolean(flags & 2), - ducking: Boolean(flags & 4), blocking: Boolean(flags & 8), - removedParts: [...limbParts, "torso"].filter((part, index) => - Boolean(flags & (1 << (index + 4)))), - score: Math.round(value(offset + 6)), - roundWins: Math.round(value(offset + 7)), attack: recentAttack(pad) }; - }); - const round = demo.rounds?.[roundIndex] || [startTick, 1, 0, 1]; - const nearEnd = tick >= endTick - 20; - const replayBall = { x: value(17), y: value(18), z: value(19), - radius: 42, active: Boolean(before[22]), spawnOwner: 0 }; - return { phase: "replay", fighters, ball: replayBall, balls: [replayBall], - camera: { x: value(23), y: value(24), width: Math.max(100, value(25)) }, - wind: { direction: round[1], mph: round[2] }, - round: { remainingMs: Math.max(0, Math.round((endTick - tick) * 1000 / - (demo.tickRate || 60))), result: nearEnd - ? demo.winner ? `${demo.winner} WINS ROUND` : "TIE" : "" } }; -} - -function applyRoundViewerState(state, now, dt = 1 / 60) { - if (!state?.fighters?.length || !state.camera || !state.round) return; - for (let index = 0; index < players.length; index++) { - const source = state.fighters[index]; - const player = players[index]; - const previousX = player.x; - const previousY = player.y; - for (const key of ["name", "color", "x", "y", "z", "facing", "alive", - "grounded", "ducking", "blocking", "score", "roundWins", "removedParts"]) - if (source[key] !== undefined) player[key] = source[key]; - player.vx = source.vx ?? (player.x - previousX) / Math.max(.001, dt); - player.vy = source.vy ?? (player.y - previousY) / Math.max(.001, dt); - player.vz = source.vz || 0; - player.attackKind = source.attack || ""; - player.attackStartedAt = player.attackKind ? now - 80000 : 0; - player.attackUntil = player.attackKind ? now + 120000 : 0; - player.hit = 0; - player.blockFlash = 0; - } - const sources = state.balls || [state.ball]; - for (let index = 0; index < balls.length; index++) { - const source = sources[index]; - if (!source) { balls[index].active = false; continue; } - for (const key of ["x", "y", "z", "radius", "active", "spawnOwner", - "type", "mass", "heldBy"]) - if (source[key] !== undefined) balls[index][key] = source[key]; - } - cameraCenter = state.camera.x; - cameraCenterY = state.camera.y; - cameraWidth = state.camera.width; - if (state.wind) { - windDirection = state.wind.direction; - windMph = state.wind.mph; - windAcceleration = windDirection * windMph * 16; - } - const hadResult = Boolean(roundResult); - roundResult = state.round.result || ""; - roundCause = state.round.cause || - (roundResult.includes("BALLED") ? "BALLED" : roundResult ? "ROUND" : ""); - roundElapsedUs = Math.max(0, roundDurationUs - state.round.remainingMs * 1000); - matchOver = state.phase === "match"; - roundStartedAt = now - introDurationUs - roundElapsedUs; - if (roundResult && !hadResult) roundOverAt = now; - const target = state.camera.target || { x: cameraCenter, y: cameraCenterY, z: 0 }; - cameraDoll.track({ target, - position: state.camera.position || - { x: cameraCenter, y: cameraCenterY, z: -cameraWidth * 1.35 }, - width: cameraWidth, perspective: state.camera.perspective || 0, - fov: state.camera.fov || 55, roll: state.camera.roll || 0 }, dt, 1000); -} - -function handleRoundViewer(message) { - const now = runtime().monotonicUs; - matchName = message.roundName || matchName; - if (message.type === "round") { - roundViewerDemo = null; - roundViewerMode = ""; - roundViewerStatus = "CONNECTING"; - return; - } - if (message.type === "status") { - roundViewerStatus = String(message.content?.label || "waiting").toUpperCase(); - return; - } - if (message.type === "demo") { - roundViewerDemo = message.content; - roundViewerDemoStartedAt = now; - roundViewerMode = "DEMO"; - return; - } - if (message.type === "state") { - if (roundViewerDemo && !message.live) return; - if (roundViewerDemo && message.content?.phase === "match") return; - roundViewerMode = "LIVE"; - applyRoundViewerState(message.content, now); - } -} - -function updateRoundViewer(now, dt) { - if (roundViewerDemo && roundViewerMode === "DEMO") { - const state = roundDemoState(roundViewerDemo, now); - if (state) applyRoundViewerState(state, now, dt); - } -} - -function gameBoot() { - syncGameView(); - startedAt = runtime().monotonicUs; - roundStartedAt = startedAt; - lastSimAt = startedAt; - roundElapsedUs = 0; - lastCountdownSecond = -1; - lastIntroSecond = 0; - resultPulseAt = 0; - resultCardStung = false; - emitSignal("hello", -1, 1, 0); - shellMode = "MENU"; - titleTransitionAt = null; - spectatorQr = typeof qrcode === "function" - ? spectatorCode("https://oskiewar.com") : null; - shellPrevious = []; - navigationPrevious = [[], []]; - roundViewer = globalThis.__oskiewarRoundBridge || null; - if (roundViewer?.start) { - shellMode = "GAME"; - selecting = false; - roundResult = ""; - matchOver = false; - matchName = roundViewer.name || ""; - spectatorQr = typeof qrcode === "function" - ? spectatorCode("https://oskiewar.com/" + matchName) : spectatorQr; - roundStartedAt = startedAt - introDurationUs; - roundViewerStop = roundViewer.start(handleRoundViewer); - return; - } - if (globalThis.__oskiewarSelfPlay) { - startSelfPlay(startedAt); - return; - } - beginTraining(startedAt); -} - -function resetRound(now, resetMatch = false) { - if (replay) { - const nextRoundName = pronounceableMatchName(); - if (matchName) publishSpectator(now, { - target: matchName, nextRoundId: "ow-" + nextRoundName, force: true, - }); - previousRoundName = matchName; - matchName = nextRoundName; - replay.roundIds.push("ow-" + matchName); - spectatorQr = typeof qrcode === "function" - ? spectatorCode("https://oskiewar.com/" + matchName) : null; - } - impacts.length = 0; - detachedParts.length = 0; - bullets.length = 0; - grenades.length = 0; - roundReplayFrames = []; - roundReplayLastAt = 0; - instantReplay = null; - replayOfferPrevious = []; - frameTelemetry = []; - frameTelemetryFlushAt = now + 1000000; - for (const player of players) { - applyRoster(player, player.rosterIndex); - player.x = player.spawnX; - player.y = floorY; - player.z = 0; - player.vx = 0; - player.vy = 0; - player.vz = 0; - player.facing = player.pad === 0 ? 1 : -1; - player.grounded = true; - player.ducking = false; - player.alive = true; - player.respawnAt = 0; - player.score = 0; - if (resetMatch) player.roundWins = 0; - player.inputX = 0; - player.inputY = 0; - player.lastTap = {}; - player.lastRelease = {}; - player.dashUntil = 0; - player.dashVx = 0; - player.runSince = 0; - player.attackKind = ""; - player.attackUntil = 0; - player.attackHit = false; - player.blocking = false; - player.blockFlash = 0; - player.shieldLocked = false; - player.shieldBrokenAt = 0; - player.windVx = 0; - player.knockVx = 0; - player.shieldVx = 0; - player.gunAmmo = 0; - player.grenadeAmmo = 0; - player.gunAimX = player.facing; - player.gunAimY = 0; - player.stance = "NEUTRAL"; - player.itemAction = ""; - player.itemActionStartedAt = 0; - player.itemActionUntil = 0; - player.heldBall = -1; - player.grabHeld = false; - player.commandStream = []; - player.hitSegment = -1; - player.hitSegmentUntil = 0; - player.hitStunUntil = 0; - player.partDamage = {}; - player.removedParts = []; - player.pogoHit = false; - player.pogoDive = false; - player.pounding = false; - player.poundFrom = 0; - player.standingOn = -1; - player.previousY = floorY; - player.crouchBlend = 0; - player.jumpLaunchAt = 0; - player.jumpPoseUntil = 0; - player.landPoseUntil = 0; - player.jumpHeld = false; - player.hopUntil = 0; - player.sinkUntil = 0; - player.crouchJump = false; - player.attackMomentum = 1; - player.botPresses = {}; - player.botAttackAt = now + 420000; - player.botItemAt = now + 600000; - player.botSinkAt = now + 900000; - player.botSinkTaps = 0; - player.botSinkNextAt = 0; - player.botAttackSequence = 0; - player.botJumpAt = now + 900000; - player.botThreatSeen = 0; - player.botShieldAt = Infinity; - player.botPunishedAt = 0; - player.botRngState = (Math.round(now / 1000) ^ - Math.imul(player.pad + 1, 0x9e3779b9)) >>> 0; - delete player.frozenGeometry; - delete player.frozenAt; - delete player.headBustedAt; - // Only a hand can still be leaning on a button across the reset; a bot's - // presses were just cleared, so inheriting pad two would suppress them. - player.previous = player.npc || player.bot ? [] - : padSnapshots[player.pad]?.down?.slice() || []; - player.suppressedDirections = player.previous.filter((button) => - button.startsWith("Arrow")); - player.lastButton = "NONE"; - player.lastButtonAt = 0; - } - for (const pickup of [...gunPickups, ...grenadePickups]) { - pickup.active = false; - pickup.respawnAt = 0; - } - for (const tree of bodyTrees) tree.growth = 0; - nextPowerupAtUs = powerupIntervalUs; - powerupSequence = 0; - roundResult = ""; - roundCause = ""; - deathCinematic = null; - matchOver = false; - roundElapsedUs = 0; - lastCountdownSecond = -1; - lastIntroSecond = 0; - resultPulseAt = 0; - resultCardStung = false; - lastSimAt = now; - roundStartedAt = now; - rollWind(now); - resetAirParticles(); - resetBalls(now); - if (replay) replay.rounds.push([demoTick(now), windDirection, windMph, - balls.length]); - cameraCenter = (worldLeft + worldRight) / 2; - cameraWidth = 1120; - cameraCenterY = floorY - cameraWidth / cameraAspect / 2; - cameraContainFloor = 0; -} - -// The box the camera packs. It is a fighting-game pushbox rather than the -// animated silhouette on purpose: breathing and stride would otherwise pump -// the lens once per idle cycle. Reach spans a fully extended punch or kick so -// throwing one never shoves the frame; live limbs still set the containment -// floor for anything this box misses. -function fighterFrameRect() { - let left = Infinity; - let right = -Infinity; - let top = Infinity; - let bottom = -Infinity; - for (const player of players) { - // Trimmed 2026-08-09 — @jeffrey wanted the lens closer to the fight. - // The rect still tracks live position, so jumps and knockbacks widen - // the frame as they happen; this is standing headroom, not arc room. - const reach = isHeadOnly(player) ? 34 : isPogo(player) ? 56 : 104; - const rise = isHeadOnly(player) ? 52 : isPogo(player) ? 134 : 188; - left = Math.min(left, player.x - reach); - right = Math.max(right, player.x + reach); - top = Math.min(top, player.y - rise); - bottom = Math.max(bottom, player.y + 8); - } - return { left, right, top, bottom }; -} - -// Rect-pack: the camera width that makes a world rect exactly fill the -// action-safe frame. Whichever axis runs out first decides the lens, so a -// stacked pair frames as tightly as a spread one. -function rectPackWidth(rect) { - const safe = actionSafeRect(); - return Math.max( - (rect.right - rect.left) * (stageRight - stageLeft) / - Math.max(1, safe.right - safe.left), - (rect.bottom - rect.top) * cameraAspect * (stageBottom - stageTop) / - Math.max(1, safe.bottom - safe.top)); -} -// A fighter reduced to a bouncing head is a few dozen units tall. Without a -// floor the pack would keep closing until one limb filled the screen. -// Lowered with the reach/rise trim above: the closest the lens may sit. -const frameFloorWidth = () => compactLayout() ? 285 : 315; - -// Terrain is flat color, so it only has to reach as far as the lens can see. -// Submitting the whole arena pushed its far corners past the native ±30000 -// coordinate guard whenever the camera closed in or sat against a wall, which -// culled the ground quad and left the clear color where the stage should be. -function terrainSpan() { - const reach = cameraDoll.width; - return { left: Math.max(worldLeft, cameraCenter - reach), - right: Math.min(worldRight, cameraCenter + reach), - top: Math.max(ceilingY, cameraCenterY - reach), - bottom: Math.min(floorY, cameraCenterY + reach) }; -} - -function updateCamera(dt) { - const rect = fighterFrameRect(); - // Look slightly ahead of fast movement so zoom starts before a fighter - // reaches the safe edge instead of reacting after the crossing. - const lookAhead = .2; - let leftDrift = 0; - let rightDrift = 0; - let upDrift = 0; - let downDrift = 0; - for (const player of players) { - const dx = (player.vx + (player.windVx || 0) + (player.knockVx || 0) + - (player.shieldVx || 0)) * lookAhead; - const dy = player.vy * lookAhead; - leftDrift = Math.min(leftDrift, dx); - rightDrift = Math.max(rightDrift, dx); - upDrift = Math.min(upDrift, dy); - downDrift = Math.max(downDrift, dy); - } - rect.left += leftDrift; - rect.right += rightDrift; - rect.top += upDrift; - rect.bottom += downDrift; - const maxWidth = Math.max(worldRight - worldLeft, - (floorY - ceilingY) * cameraAspect); - const desiredWidth = clamp(rectPackWidth(rect), frameFloorWidth(), maxWidth); - const widthSpeed = desiredWidth > cameraWidth ? 13 : 5.5; - const widthBlend = 1 - Math.exp(-Math.max(0, dt) * widthSpeed); - cameraWidth += (desiredWidth - cameraWidth) * widthBlend; - const halfWidth = cameraWidth / 2; - const halfHeight = cameraWidth / cameraAspect / 2; - // Center comes from the same rect the width did, so the frame pans down as - // the action drops and rides up onto the platform with it. - let desiredCenter = cameraWidth >= worldRight - worldLeft - ? (worldLeft + worldRight) / 2 - : clamp((rect.left + rect.right) / 2, - worldLeft + halfWidth, worldRight - halfWidth); - // Feet plant a capsule radius under the floor line, and the ground plane - // keeps covering that far past it, so the frame may sit that low rather - // than widening just to recentre a standing pair. - const footRoom = 40; - let desiredCenterY = halfHeight * 2 >= floorY - ceilingY - ? (ceilingY + floorY) / 2 - : clamp((rect.top + rect.bottom) / 2, - ceilingY + halfHeight, floorY + footRoom - halfHeight); - // Fold containment into the target before easing. Clamping the live camera - // after easing caused a one-frame reset whenever a fighter crossed the safe - // edge; the pack width now absorbs that motion while the center remains - // continuous. - const containLeft = rect.right - halfWidth * .92; - const containRight = rect.left + halfWidth * .92; - if (containLeft <= containRight) - desiredCenter = clamp(desiredCenter, containLeft, containRight); - const containTop = rect.bottom - halfHeight * .92; - const containBottom = rect.top + halfHeight * .92; - if (containTop <= containBottom) - desiredCenterY = clamp(desiredCenterY, containTop, containBottom); - const centerBlend = 1 - Math.exp(-Math.max(0, dt) * 9); - cameraCenter += (desiredCenter - cameraCenter) * centerBlend; - cameraCenterY += (desiredCenterY - cameraCenterY) * centerBlend; -} - -function updateCameraDoll(dt, now) { - const introAge = now - roundStartedAt; - if (roundResult) { - const age = Math.max(0, (now - roundOverAt) / 1000000); - if (deathCinematic && age < 1.45) { - if (age < .11) return; - const loser = players[deathCinematic.loserPad]; - const winner = players[deathCinematic.winnerPad]; - const loserHead = loser.frozenGeometry?.head || - runnerWorldGeometry(loser, (now - startedAt) / 1000000).head; - const winnerHead = winner?.frozenGeometry?.head || (winner - ? runnerWorldGeometry(winner, (now - startedAt) / 1000000).head - : { x: cameraCenter, y: cameraCenterY, z: 0 }); - // The killcam used to sit inside the winner's own head, so the winner - // had to be culled or their body would have filled the lens — which is - // why the fighter who had just won blinked out for three quarters of a - // second. It stands off their shoulder now: back along the line between - // the two of them, raised, and aimed at a point most of the way to the - // fighter going down. Both are in the frustum, so nothing is hidden to - // make the shot and nobody vanishes. - // - // Every distance is drawn from the gap between the heads, because the - // pair can be a step apart or the width of the stage apart, and a fixed - // stand-off frames only one of those. - const shoulderSide = Math.sign(loserHead.x - winnerHead.x) || 1; - const headGap = Math.max(320, Math.abs(loserHead.x - winnerHead.x)); - const shoulder = { x: winnerHead.x - shoulderSide * headGap * .62, - y: winnerHead.y - headGap * .34, - z: winnerHead.z - headGap * 1.3 }; - // Aimed past the winner rather than at the loser: dead centre on the - // loser is what threw the winner off the edge of the frame. - const focus = { x: lerp(winnerHead.x, loserHead.x, .78), - y: lerp(winnerHead.y, loserHead.y, .78), - z: lerp(winnerHead.z, loserHead.z, .78) }; - // Wider than the old close-up had to be, because it now has to hold two - // fighters rather than one — the 64° lens buys most of that back. Past - // about a 1300 gap this would open onto empty stage, so beyond there it - // settles toward the span the returning wide shot uses. - const shotWidth = Math.min(headGap * 1.7, headGap + 900); - if (age < .86) { - cameraDoll.track({ target: focus, position: shoulder, - width: shotWidth, perspective: .82, fov: 64, roll: 0 }, dt, 11); - return; - } - const returnAmount = clamp((age - .86) / .59, 0, 1); - const midpoint = { x: (players[0].x + players[1].x) / 2, - y: (players[0].y + players[1].y) / 2 - 95, - z: (players[0].z + players[1].z) / 2 }; - const span = Math.max(900, Math.abs(players[1].x - players[0].x) + 540); - cameraDoll.track({ target: midpoint, - position: { x: lerp(shoulder.x, midpoint.x, returnAmount), - y: lerp(shoulder.y, midpoint.y - span * .08, returnAmount), - z: lerp(shoulder.z, midpoint.z - span * 1.2, returnAmount) }, - width: lerp(shotWidth, span, returnAmount), - perspective: lerp(.82, .72, returnAmount), fov: 50, roll: 0 }, dt, 10); - return; - } - const target = { x: (players[0].x + players[1].x) / 2, - y: (players[0].y + players[1].y) / 2 - 95, - z: (players[0].z + players[1].z) / 2 }; - const horizontalSpan = Math.abs(players[1].x - players[0].x); - const verticalSpan = Math.abs(players[1].y - players[0].y) * cameraAspect; - const closeWidth = Math.max(820, horizontalSpan + 540, verticalSpan + 520); - const orbit = Math.sin(age * .72) * closeWidth * .075; - cameraDoll.track({ target, - position: { x: target.x + orbit, - y: target.y - closeWidth * .08 + Math.sin(age * .8) * closeWidth * .035, - z: target.z - closeWidth * 1.2 }, - width: closeWidth, perspective: clamp(age / .7, 0, .72), fov: 50, - roll: Math.sin(age * .9) * .008 }, dt, 7); - return; - } - if (introAge < introDurationUs) { - const age = introAge / 1000000; - const progress = clamp(age / (introDurationUs / 1000000), 0, 1); - const eased = progress * progress * (3 - progress * 2); - const target = { x: (players[0].x + players[1].x) / 2, - y: (players[0].y + players[1].y) / 2 - 90, - z: (players[0].z + players[1].z) / 2 }; - const span = Math.max(980, Math.abs(players[1].x - players[0].x) + 760); - const angle = lerp(-.3, .3, eased); - cameraDoll.track({ target, - position: { x: target.x + Math.sin(angle) * span * .34, - y: target.y - lerp(90, 150, eased), - z: target.z - Math.cos(angle) * span * .72 }, - width: span, perspective: clamp(progress / .34, 0, 1), fov: 50 }, dt, 7); - return; - } - const target = { x: cameraCenter, y: cameraCenterY, z: 0 }; - // Measure complete animated silhouettes before rendering. This used to run - // as a final paint-time correction, which made the viewport skip a frame at - // the safe-zone edge. - const containmentWidth = fighterContainmentRequiredWidth( - (now - startedAt) / 1000000) * 1.04; - cameraContainFloor = Math.max(cameraContainFloor, containmentWidth); - // A small overscan absorbs animated hands, feet, and perspective before - // they reach the action-safe edge without loosening the close fight shot. - const naturalWidth = cameraWidth * 1.015; - // Hysteresis prevents a fighter hovering at the safe edge from repeatedly - // switching between close and wide framing. The band and the pushbox above - // are tuned together so the floor never has to release during an idle cycle; - // if it does, the lens breathes in time with the fighter's own animation. - if (cameraContainFloor > naturalWidth && - naturalWidth < cameraContainFloor * .92) { - const release = 1 - Math.exp(-Math.max(0, dt) * 1.6); - cameraContainFloor = lerp(cameraContainFloor, naturalWidth, release); - } - const framedWidth = Math.max(naturalWidth, cameraContainFloor); - // Gameplay camera is intentionally inertial but not handheld. No procedural - // swivel, roll, or dolly motion is allowed to move a stationary viewport. - const tilt = .026; - const dolly = 1.35; - cameraDoll.track({ target, - position: { x: cameraCenter, - y: cameraCenterY - framedWidth * tilt, z: -framedWidth * dolly }, - width: framedWidth, perspective: .1, fov: 55, - roll: 0 }, dt, 10); -} - -function freezeFinalFrame(now) { - const poseTime = (now - startedAt) / 1000000; - for (const player of players) { - player.frozenGeometry = runnerWorldGeometry(player, poseTime); - player.frozenAt = now; - } - impactHitboxesUntil = Math.max(impactHitboxesUntil, - now + (matchOver ? matchResultUs : roundResultUs)); -} - -function finishRound(now) { - if (roundResult) return; - freezeFinalFrame(now); - captureRoundReplay(now, true); - let roundPan = 0; - if (players[0].score === players[1].score) { - roundResult = "TIE"; - emitSignal("tie", -1, players[0].score, players[1].score); - } - else { - const winner = players[0].score > players[1].score ? players[0] : players[1]; - roundPan = panPlayer(winner); - winner.roundWins += 1; - matchOver = winner.roundWins >= matchWins; - roundResult = winner.name + (matchOver ? " WINS MATCH" : " WINS ROUND"); - emitSignal(matchOver ? "matchwin" : "roundwin", winner.pad, - winner.roundWins, winner.score); - } - impactHitboxesUntil = Math.max(impactHitboxesUntil, - now + (matchOver ? matchResultUs : roundResultUs)); - roundOverAt = now; - for (const player of players) player.vx = 0; - playDrum("clap", 1.2, roundPan); - captureFrameTelemetry(now, true); - saveRoundReplay(now); - if (matchOver) finishReplay(); -} - -function resultCardText() { - if (roundResult === "TIE") return { winner: "tie", action: "" }; - const encoded = roundResult.match(/^(@\S+)\s+WINS\b/i); - const winner = encoded?.[1] || - (players[0].score > players[1].score ? players[0].name : players[1].name); - const actions = { BALLED: "balled", KO: "knocked out", TRADE: "trade", - TIME: "time", ROUND: "" }; - return { winner: winner.toLowerCase() + " wins!", - action: actions[roundCause] ?? roundCause.toLowerCase() }; -} - -function quantizedInput(pad, suppressed = []) { - const held = pad.down; - const active = (button) => held.includes(button) && !suppressed.includes(button); - let horizontal = (active("ArrowRight") ? 1 : 0) - - (active("ArrowLeft") ? 1 : 0); - let vertical = (active("ArrowUp") ? 1 : 0) - - (active("ArrowDown") ? 1 : 0); - if (!horizontal && Math.abs(pad.leftX) >= 0.48) horizontal = pad.leftX > 0 ? 1 : -1; - if (!vertical && Math.abs(pad.leftY) >= 0.48) vertical = pad.leftY > 0 ? 1 : -1; - return { horizontal, vertical }; -} - -function remember(player, button) { - player.lastButton = buttonLabel(button); - player.lastButtonAt = runtime().monotonicUs; - const command = { A: "/", B: "*", X: ")", Y: "+", - LeftShoulder: "+", RightShoulder: "+" }[button] || player.lastButton; - if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", - "A", "B", "X", "Y", "LeftShoulder", "RightShoulder"].includes(button)) - recordCommand(player, command, player.lastButtonAt); - telemetry("FIGHT_BUTTON", player.name + " " + player.lastButton); -} - -function recordCommand(player, label, now) { - const previous = player.commandStream.at(-1); - if (previous?.label === label && now - previous.at < 40000) return; - player.commandStream.push({ label, at: now }); - while (player.commandStream.length > commandStreamDepth) - player.commandStream.shift(); -} - -function fireGun(player, input) { - const now = runtime().monotonicUs; - const pose = gunPose(player, now, input); - player.gunAimX = pose.dx; - player.gunAimY = pose.dy; - bullets.push({ - x: pose.muzzle.x, y: pose.muzzle.y, z: pose.muzzle.z, - vx: pose.dx * 2600, vy: pose.dy * 2600, - owner: player.pad, life: 1.8, - }); - while (bullets.length > 24) bullets.shift(); - player.gunAmmo -= 1; - player.itemAction = "FIRE"; - player.itemActionStartedAt = now; - player.itemActionUntil = now + 170000; - player.pendingMoveLabel = "FIRE " + player.gunAmmo; - playDrum("hat", 1.05, panPlayer(player)); - emitSignal("bullet", player.pad, pose.dx, pose.dy); -} - -function throwGrenade(player) { - const now = runtime().monotonicUs; - grenades.push({ x: player.x + player.facing * 150, - y: player.y - (player.ducking ? 80 : 145), z: player.z, - vx: player.facing * 1850, vy: -720, owner: player.pad, - fuse: 1.15, alive: true, exploding: false, blastAge: 0, blastRadius: 0, - hitPlayers: 0 }); - while (grenades.length > 12) grenades.shift(); - player.grenadeAmmo -= 1; - player.itemAction = "THROW"; - player.itemActionStartedAt = now; - player.itemActionUntil = now + 260000; - player.pendingMoveLabel = "GRENADE " + player.grenadeAmmo; - playDrum("kick", .95, panPlayer(player)); - emitSignal("grenade", player.pad, player.facing, player.ducking ? 1 : 0); -} - -function updateGunPickups(now) { - const poseTime = (now - startedAt) / 1000000; - for (const pickup of gunPickups) { - if (!pickup.active) continue; - for (const player of players) { - if (!player.alive || runnerDistanceToPoint(player, poseTime, - pickup.x, pickup.y, pickup.z) > 90) continue; - player.gunAmmo = Math.min(12, player.gunAmmo + pickup.amount); - pickup.active = false; - remember(player, "GUN +" + pickup.amount); - playDrum("clap", 1.1, panPlayer(player)); - emitSignal("pickup", player.pad, 1, pickup.amount); - break; - } - } -} - -function updateGrenadePickups(now) { - const poseTime = (now - startedAt) / 1000000; - for (const pickup of grenadePickups) { - if (!pickup.active) continue; - for (const player of players) { - if (!player.alive || runnerDistanceToPoint(player, poseTime, - pickup.x, pickup.y, pickup.z) > 90) continue; - player.grenadeAmmo = Math.min(4, player.grenadeAmmo + pickup.amount); - pickup.active = false; - remember(player, "GRENADE +" + pickup.amount); - playDrum("clap", 1.1, panPlayer(player)); - emitSignal("pickup", player.pad, 2, pickup.amount); - break; - } - } -} - -// Ripe fruit is a new body and fifteen seconds. The clock is `roundElapsedUs` -// counting up toward `roundDurationUs`, so winding it back is what "adds -// time" means here — and it is clamped at zero, because a tree cannot make -// the round longer than a round. -// The tree leans in off its wall, so both the picture and the pickup test have -// to agree about where the fruit ended up. One function owns that. -// The tree climbs its wall more than it leans off it, because a fighter pinned -// against that wall still has to be able to reach the fruit from a jump. One -// function owns the geometry so the picture and the pickup test agree. -function treeFruit(tree) { - const toward = tree.x < (worldLeft + worldRight) / 2 ? 1 : -1; - const reach = 90 + 330 * tree.growth; - return { x: tree.x + toward * (reach * .5 + 60), y: tree.y - reach * .9, - z: tree.z, toward, reach, - tipX: tree.x + toward * reach * .5, tipY: tree.y - reach * .9 }; -} - -function updateBodyTrees(dt, now) { - const poseTime = (now - startedAt) / 1000000; - for (const tree of bodyTrees) { - if (tree.growth < 1) { - tree.growth = Math.min(1, tree.growth + dt * 1000000 / treeRipenUs); - continue; - } - const fruit = treeFruit(tree); - for (const player of players) { - if (!player.alive) continue; - if (runnerDistanceToPoint(player, poseTime, fruit.x, fruit.y, fruit.z) > 320) - continue; - tree.growth = 0; - player.removedParts = []; - player.partDamage = {}; - roundElapsedUs = Math.max(0, roundElapsedUs - treeTimeBonusUs); - remember(player, "NEW BODY"); - playDrum("bell", 1.15, panPlayer(player)); - emitSignal("powerup", player.pad, 3, treeTimeBonusUs / 1000000); - break; - } - } -} - -function drawBodyTree(tree, t) { - const scale = cameraScale(); - const ripe = tree.growth >= 1; - // Unripe reads as a sapling rather than a dimmed icon: the trunk and canopy - // are literally shorter, so the stage tells you how long the walk can wait. - const { x: fruitX, y: fruitY, toward, tipX, tipY: crownY } = treeFruit(tree); - const sway = Math.sin(t * 1.4 + tree.x * .0007) * (3 + 9 * tree.growth); - const bark = mixColor([120, 92, 64], [58, 46, 38], visualTheme.light); - const leaf = ripe - ? mixColor([120, 226, 138], [46, 150, 84], visualTheme.light) - : mixColor([96, 140, 104], [44, 84, 60], visualTheme.light); - const tipY = crownY + sway; - worldCapsule(tree.x, tree.y, tree.z, tipX, tipY, tree.z, - Math.max(2, (11 + 9 * tree.growth) * scale), bark); - for (const spread of [-1, -.35, .35, 1]) { - worldCapsule(tipX, tipY, tree.z, - tipX + toward * (60 + 90 * tree.growth), - tipY + spread * (60 + 70 * tree.growth), tree.z, - Math.max(2, (8 + 8 * tree.growth) * scale), leaf); - } - if (!ripe) return; - // The fruit is the part you aim at, so it only exists once it can be taken. - const point = projectPoint(fruitX, fruitY + sway, tree.z); - filledDisc(point.x, point.y, Math.max(3, 46 * scale), - mixColor([236, 108, 132], [188, 62, 92], visualTheme.light)); -} - -function updatePowerups(now) { - while (roundElapsedUs >= nextPowerupAtUs) { - const occupied = [...gunPickups, ...grenadePickups] - .some((pickup) => pickup.active); - if (!occupied) { - const choices = [gunPickups[0], grenadePickups[0]]; - const pickup = choices[powerupSequence % choices.length]; - pickup.active = true; - // The flag owns exact center. Alternating pickup lanes keep a real-sized - // weapon from reading as hardware attached to its pole. - pickup.x = (platformLeft + platformRight) / 2 + - (powerupSequence % 2 ? 1400 : -1400); - pickup.y = PLATFORM ? platformY - 70 : floorY - 35; - pickup.z = 0; - powerupSequence += 1; - emitSignal("powerup", -1, powerupSequence, nextPowerupAtUs / 1000000); - playDrum("clap", .9, 0); - } - nextPowerupAtUs += powerupIntervalUs; - } - updateGunPickups(now); - updateGrenadePickups(now); -} - -function updateBullets(dt, now) { - for (const bullet of bullets) { - if (bullet.life <= 0) continue; - bullet.vx += windAcceleration * .12 * dt; - bullet.x += bullet.vx * dt; - bullet.y += bullet.vy * dt; - bullet.life -= dt; - if (bullet.x - 24 <= worldLeft + wallThickness || - bullet.x + 24 >= worldRight - wallThickness || - bullet.y - 24 <= ceilingY + wallThickness || - bullet.y + 24 >= floorY - wallThickness) bullet.life = 0; - } - for (let left = 0; left < bullets.length; left++) { - const a = bullets[left]; - if (a.life <= 0) continue; - for (let right = left + 1; right < bullets.length; right++) { - const b = bullets[right]; - if (b.life <= 0 || a.owner === b.owner) continue; - if (Math.abs(a.x - b.x) <= 96 && Math.abs(a.y - b.y) <= 52 && - Math.abs(a.z - b.z) <= 48) { - a.life = 0; - b.life = 0; - impacts.push({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, - z: (a.z + b.z) / 2, life: .18, duration: .18, - death: false, explosion: false }); - playDrum("hat", 1, 0); - emitSignal("cancel", -1, a.owner, b.owner); - break; - } - } - } - const poseTime = (now - startedAt) / 1000000; - for (const bullet of bullets) { - if (bullet.life <= 0) continue; - const target = players[bullet.owner === 0 ? 1 : 0]; - if (!target.alive) continue; - // A raised shield answers a bullet by sending it home, and hands it over: - // flipping `owner` is what lets the returned shot hurt whoever fired it. - if (target.blocking) { - const shield = shieldGeometry(target); - if (Math.hypot(bullet.x - shield.x, bullet.y - shield.y, - bullet.z - shield.z) <= shield.radius + 24) { - bullet.vx = -bullet.vx; - bullet.vy = -Math.abs(bullet.vy) * .35; - bullet.owner = target.pad; - bullet.life = Math.max(bullet.life, .55); - impacts.push({ x: bullet.x, y: bullet.y, z: bullet.z, - life: .18, duration: .18, death: false, explosion: false }); - breakShield(target, now); - emitSignal("ballblock", target.pad, 1, 0); - continue; - } - } - const contact = runnerContactToPoint(target, poseTime, - bullet.x, bullet.y, bullet.z); - if (Math.min(contact.headDistance, contact.bodyDistance) <= 24) { - bullet.life = 0; - impacts.push({ x: bullet.x, y: bullet.y, z: bullet.z, - life: .2, duration: .2, death: contact.headDistance <= 24, - explosion: false }); - impactHitboxesUntil = Math.max(impactHitboxesUntil, now + 350000); - if (contact.headDistance <= 24) - killPlayer(target, bullet.owner, now, "SHOT"); - else { - applyBodyHit(target, contact.segmentIndex, - bullet.x - bullet.vx, bullet.owner, now, 1180, 125); - playDrum("block", .9, panPlayer(target)); - } - } - } - for (let index = bullets.length - 1; index >= 0; index--) - if (bullets[index].life <= 0) bullets.splice(index, 1); -} - -function updateGrenades(dt, now) { - for (const grenade of grenades) { - if (!grenade.alive) continue; - if (grenade.exploding) { - grenade.blastAge += dt; - grenade.blastRadius = grenadeBlastRadius * - Math.min(1, grenade.blastAge / grenadeBlastDuration); - const poseTime = (now - startedAt) / 1000000; - for (const player of players) { - if (!player.alive || (grenade.hitPlayers & (1 << player.pad))) continue; - const contact = runnerContactToPoint(player, poseTime, - grenade.x, grenade.y, grenade.z); - if (Math.min(contact.headDistance, contact.bodyDistance) > - grenade.blastRadius) continue; - grenade.hitPlayers |= 1 << player.pad; - if (contact.headDistance <= grenade.blastRadius) - killPlayer(player, grenade.owner, now, "BLASTED"); - else { - const blastForce = 1250 + 850 * - (1 - clamp(contact.bodyDistance / grenadeBlastRadius, 0, 1)); - applyBodyHit(player, contact.segmentIndex, grenade.x, - grenade.owner, now, blastForce, 300); - } - } - if (grenade.blastAge >= grenadeBlastDuration) grenade.alive = false; - continue; - } - const previousY = grenade.y; - grenade.vx += windAcceleration * .45 * dt; - grenade.vy += 1800 * dt; - grenade.x += grenade.vx * dt; - grenade.y += grenade.vy * dt; - grenade.fuse -= dt; - const inset = wallThickness + 35; - if (grenade.x < worldLeft + inset) { - grenade.x = worldLeft + inset; - grenade.vx = Math.abs(grenade.vx) * .65; - } else if (grenade.x > worldRight - inset) { - grenade.x = worldRight - inset; - grenade.vx = -Math.abs(grenade.vx) * .65; - } - if (grenade.y < ceilingY + inset) { - grenade.y = ceilingY + inset; - grenade.vy = Math.abs(grenade.vy) * .65; - } - if (PLATFORM && grenade.vy >= 0 && previousY <= platformY - 30 && - grenade.y >= platformY - 30 && grenade.x >= platformLeft && - grenade.x <= platformRight) { - grenade.y = platformY - 30; - grenade.vy = -Math.abs(grenade.vy) * .55; - grenade.vx *= .82; - } else if (grenade.y >= floorY - 30) { - grenade.y = floorY - 30; - grenade.vy = -Math.abs(grenade.vy) * .55; - grenade.vx *= .82; - } - if (grenade.fuse <= 0) { - grenade.exploding = true; - grenade.blastAge = 0; - grenade.blastRadius = 0; - grenade.vx = 0; - grenade.vy = 0; - playDrum("kick", 1.25, panPlayer(players[grenade.owner])); - emitSignal("blast", grenade.owner, - grenade.x / worldRight, grenade.y / floorY); - } - } - for (let index = grenades.length - 1; index >= 0; index--) - if (!grenades[index].alive) grenades.splice(index, 1); -} - -function startMelee(player, kind, now) { - if (isHeadOnly(player) || isPogo(player)) return; - const spec = meleeSpecs[kind]; - if (!spec) return; - const attackingPart = kind === "KICK" - ? player.facing > 0 ? "right-leg" : "left-leg" - : itemHand(player); - if (!hasPart(player, attackingPart)) return; - player.attackKind = kind; - player.attackStartedAt = now; - player.attackUntil = now + spec.windowUs; - player.attackHit = false; - player.attackMomentum = now < player.dashUntil - ? clamp(Math.abs(player.vx) / walkSpeed, 1.35, 2.25) : 1; - player.stance = "ATTACK"; - player.pendingMoveLabel = kind; - playDrum(spec.cue[0], spec.cue[1], panPlayer(player)); - emitSignal(kind.toLowerCase(), player.pad, player.facing, 0); -} - -const limbParts = ["left-arm", "right-arm", "left-leg", "right-leg"]; -const hasPart = (player, part) => !player.removedParts?.includes(part); -const isPogo = (player) => hasPart(player, "torso") && - limbParts.every((part) => !hasPart(player, part)); -const isHeadOnly = (player) => !hasPart(player, "torso"); -// A weapon rides in the lead hand — the same arm the pose already hangs it on. -// Lose that arm and the fighter is disarmed until they turn around. -const itemHand = (player) => player.facing > 0 ? "right-arm" : "left-arm"; -// Gun before grenade, always: the pistol is the sustained item and the one -// drawn in the hand, so "use what you are holding" needs no mode toggle. -const heldItem = (player) => - isHeadOnly(player) ? player.gunAmmo > 0 ? "GUN" : "" - : isPogo(player) || !hasPart(player, itemHand(player)) - ? "" : player.gunAmmo > 0 ? "GUN" - : player.grenadeAmmo > 0 ? "GRENADE" : ""; -const itemSwinging = (player, now) => - (player.attackKind === "WHIP" || player.attackKind === "BASH") && - meleePulse(player, now) > 0; - -function meleePulse(player, now) { - if (now >= player.attackUntil || player.attackUntil <= player.attackStartedAt) return 0; - const phase = (now - player.attackStartedAt) / - (player.attackUntil - player.attackStartedAt); - return Math.sin(Math.max(0, Math.min(1, phase)) * Math.PI); -} - -function meleeTarget(player, now) { - const pulse = meleePulse(player, now); - const spec = meleeSpecs[player.attackKind] || meleeSpecs.PUNCH; - return { - x: player.x + player.facing * (spec.reach + spec.swell * pulse), - y: player.y - spec.height, z: player.z, - }; -} - -function itemActionPulse(player, now) { - if (!player.itemAction || now >= player.itemActionUntil) return 0; - const phase = (now - player.itemActionStartedAt) / - Math.max(1, player.itemActionUntil - player.itemActionStartedAt); - return Math.sin(clamp(phase, 0, 1) * Math.PI); -} - -function itemHandTarget(player, now) { - // A surviving head grips the pistol at the mouth. It can still aim and fire, - // but grenades and melee weapons remain limb-dependent. - if (isHeadOnly(player)) return { - x: player.x + player.facing * 18, - y: player.y - 20, z: player.z, - }; - // A swung weapon rides the striking hand, so the drawn pistol or grenade - // sits on the live attack capsule instead of floating out in front. - if (itemSwinging(player, now)) return meleeTarget(player, now); - const pulse = itemActionPulse(player, now); - if (player.itemAction === "THROW") return { - x: player.x + player.facing * (42 + 52 * pulse), - y: player.y - 118 - 52 * pulse, z: player.z, - }; - return { x: player.x + player.facing * 108, - y: player.y - 115, z: player.z }; -} - -function gunPose(player, now, input = null) { - let aimX = input?.horizontal || player.facing; - let aimY = input ? -input.vertical : 0; - if (!input && player.itemAction === "FIRE" && now < player.itemActionUntil) { - aimX = player.gunAimX || player.facing; - aimY = player.gunAimY || 0; - } - const length = Math.hypot(aimX, aimY) || 1; - const dx = aimX / length; - const dy = aimY / length; - const hand = itemHandTarget(player, now); - return { - hand, dx, dy, - muzzle: { x: hand.x + dx * 54, y: hand.y + dy * 54, z: hand.z }, - }; -} - -function meleeStrike(player, now) { - const target = meleeTarget(player, now); - return { - x: target.x, y: target.y, z: target.z, - radius: (meleeSpecs[player.attackKind] || meleeSpecs.PUNCH).radius, - }; -} - -function shieldGeometry(player) { - return { - x: player.x + player.facing * shieldForward, - y: player.y - 90, - z: player.z, - radius: shieldRadius, - }; -} - -// A shield is spent the moment it does its job. It eats exactly one hit, then -// drops — and `shieldLocked` keeps a still-held B from raising it again on the -// very next frame, which is what turns the break into a real opening instead -// of a flicker nobody can see. The shielder is deliberately left free to act: -// breaking the shield is what buys them the swing. -function breakShield(player, now) { - player.blocking = false; - player.shieldLocked = true; - player.shieldBrokenAt = now; - player.blockFlash = 1; - player.stance = "SHIELD BREAK"; - player.pendingMoveLabel = "SHIELD BREAK"; - playDrum("bell", 1, panPlayer(player)); - emitSignal("shieldbreak", player.pad, 1, 0); -} - -// Stun scales with what the shield ate, so a BASH buys a longer punish than a -// WHIP. The floor is still long enough to answer at 60fps (~8 frames) and the -// ceiling short enough (~18) that a blocked attacker is opened, not deleted. -function shieldStunUs(force) { - return Math.round(140000 + clamp((force - 1000) / 750, 0, 1) * 160000); -} - -function returnBall(ball, player, now, shielded, intensity = 1) { - const incomingVx = ball.vx; - const incomingVy = ball.vy; - const direction = ball.x >= player.x ? 1 : -1; - const currentSpeed = Math.hypot(ball.vx, ball.vy); - const response = ball.hitScale || 1; - const momentum = shielded ? 1 : player.attackMomentum || 1; - const normalSpeed = (currentSpeed * 1.34 + 720) * intensity * - momentum * response; - const shieldMinimum = (1800 + clamp(intensity, 0, 1) * 1000) * response; - const speed = shielded - ? Math.min(4200 * response, Math.max(shieldMinimum, normalSpeed * 1.65)) - : Math.min(4800, normalSpeed); - ball.vx = direction * speed; - const lift = shielded ? .72 : player.inputY > 0 ? .58 - : player.attackKind === "KICK" ? .34 : .2; - ball.vy = -speed * lift; - ball.x = player.x + direction * (ball.radius + 85); - ball.y = Math.min(ball.y, player.y - 55); - ball.lastHitBy = player.pad; - ball.safeUntil = now + 140000; - ball.safePlayers = 1 << player.pad; - if (!shielded) { - player.attackHit = true; - player.lastButton = "WACK"; - player.lastButtonAt = now; - } - else { - player.blockFlash = 1; - player.knockVx += incomingVx * .55; - player.vy = Math.min(player.vy + incomingVy * .16, -Math.max(520, speed * .28)); - player.grounded = false; - } - impacts.push({ x: ball.x, y: ball.y, z: ball.z, - life: .22, duration: .22, death: false, explosion: false }); - impactHitboxesUntil = Math.max(impactHitboxesUntil, now + 350000); - playDrum(shielded ? "block" : "clap", shielded ? 1.1 : 1.25, - panAt(ball.x, ball.z)); - emitSignal(shielded ? "ballblock" : "wack", player.pad, - direction, Math.round(speed)); -} - -function crossWackBall(ball, hitters, now) { - const first = hitters[0]; - const second = hitters[1]; - const fighterDistance = Math.abs(first.player.x - second.player.x); - const convergence = clamp(1 - fighterDistance / 520, 0, 1); - const contact = (first.contact + second.contact) * .5; - const currentSpeed = Math.hypot(ball.vx, ball.vy); - const speed = Math.min(7600, (4100 + currentSpeed * .72 + - convergence * 2300 + contact * 900) * (ball.hitScale || 1)); - const stronger = first.contact >= second.contact ? first.player : second.player; - const direction = Math.sign(ball.vx) || stronger.facing || 1; - ball.vx = direction * speed; - ball.vy = -speed * (.3 + convergence * .18); - ball.x += direction * 35; - ball.y = Math.min(ball.y, floorY - ball.radius - 8); - ball.lastHitBy = -1; - ball.safeUntil = now + 180000; - ball.safePlayers = 3; - for (const hit of hitters) { - hit.player.attackHit = true; - hit.player.lastButton = "CROSS WACK"; - hit.player.lastButtonAt = now; - } - impacts.push({ x: ball.x, y: ball.y, z: ball.z, - life: .32, duration: .32, death: false, explosion: true }); - impactHitboxesUntil = Math.max(impactHitboxesUntil, now + 350000); - playDrum("clap", 1.55, panAt(ball.x, ball.z)); - emitSignal("crosswack", -1, direction, Math.round(speed)); -} - -function bootBall(ball, player, now) { - // Walking contact carries the grounded ball in the player's travel - // direction. A melee strike remains the intentionally stronger launch. - const direction = Math.sign(player.vx) || Math.sign(ball.x - player.x) || - player.facing || 1; - const dashing = now < player.dashUntil && Math.abs(player.dashVx) > walkSpeed; - const speed = Math.min(dashing ? 2600 : 1450, - (420 + Math.abs(player.vx) * (dashing ? .58 : .32)) * - (ball.hitScale || 1)); - ball.vx = direction * speed; - ball.vy = -Math.max(dashing ? 420 : 80, speed * (dashing ? .24 : .06)); - ball.x = player.x + direction * (ball.radius + 58); - ball.y = Math.min(ball.y, floorY - ball.radius - 2); - ball.lastHitBy = player.pad; - ball.safeUntil = now + 180000; - ball.safePlayers = 1 << player.pad; - player.lastButton = dashing ? "DASH BOOT" : "BOOT"; - player.lastButtonAt = now; - impacts.push({ x: ball.x, y: ball.y, z: ball.z, - life: .16, duration: .16, death: false, explosion: false }); - impactHitboxesUntil = Math.max(impactHitboxesUntil, now + 300000); - playDrum("kick", 1.12, panAt(ball.x, ball.z)); - emitSignal("boot", player.pad, direction, Math.round(speed)); -} - -function bounceBallOffBody(ball, player, now, segmentIndex = -1) { - const direction = Math.sign(ball.x - player.x) || -Math.sign(ball.vx) || - player.facing || 1; - const incomingSpeed = Math.hypot(ball.vx, ball.vy); - const speed = Math.max(760, Math.min(3600, incomingSpeed * .86 / - Math.sqrt(ball.mass || 1))); - ball.vx = direction * speed; - ball.vy = -Math.max(260, Math.abs(ball.vy) * .42 + speed * .12); - ball.x += direction * (ball.radius + 18); - ball.lastHitBy = player.pad === 0 ? 1 : 0; - ball.safeUntil = now + 160000; - ball.safePlayers = 1 << player.pad; - applyBodyHit(player, segmentIndex, ball.x - ball.vx, - ball.lastHitBy, now, 760, 110, false); - impacts.push({ x: ball.x, y: ball.y, z: ball.z, - life: .16, duration: .16, death: false, explosion: false }); - impactHitboxesUntil = Math.max(impactHitboxesUntil, now + 300000); - playDrum("block", .82, panAt(ball.x, ball.z)); - emitSignal("bodybounce", player.pad, direction, Math.round(speed)); -} - -function updateBall(ball, dt, now) { - // A popped ball is gone, not retired — a crater should cost the round its - // ball for a moment, not for good. It re-inflates over the middle once its - // serve time comes round. - if (!ball.active && ballEnabled && ball.serveAt && now >= ball.serveAt) { - ball.active = true; - ball.x = (platformLeft + platformRight) / 2; - ball.y = ceilingY + ball.radius + 120; - ball.z = 0; - ball.vx = 0; - ball.vy = 0; - ball.rotation = 0; - ball.lastHitBy = -1; - ball.safeUntil = now + 250000; - ball.safePlayers = 0; - emitSignal("ballserve", -1, 0, 0); - } - if (!ball.active || now < ball.serveAt) return; - if (ball.heldBy >= 0) { - const carrier = players[ball.heldBy]; - if (!carrier?.alive || carrier.heldBall !== balls.indexOf(ball)) { - ball.heldBy = -1; - } else { - ball.x = carrier.x + carrier.facing * (ball.radius + 54); - ball.y = carrier.y - (carrier.ducking ? 58 : 94); - ball.z = carrier.z - 6; - ball.vx = carrier.vx; - ball.vy = carrier.vy; - return; - } - } - const platformSupported = PLATFORM && ball.x >= platformLeft + ball.radius && - ball.x <= platformRight - ball.radius && - ball.y >= platformY - ball.radius - 2 && - ball.y <= platformY - ball.radius + 2; - const floorSupported = ball.y >= floorY - ball.radius - 2; - const grounded = (platformSupported || floorSupported) && Math.abs(ball.vy) < 180; - if (!grounded) ball.vx += windAcceleration * (ball.windFactor || .45) * dt; - ball.vy += 1900 * (ball.gravityFactor || 1) * dt; - const previous = { x: ball.x, y: ball.y, z: ball.z }; - ball.x += ball.vx * dt; - ball.y += ball.vy * dt; - if (Math.abs(ball.vx) > 20) - ball.rotation += ball.vx * dt / Math.max(1, ball.radius); - const inset = wallThickness + ball.radius; - if (ball.x < worldLeft + inset) { - ball.x = worldLeft + inset; - ball.vx = Math.abs(ball.vx); - } else if (ball.x > worldRight - inset) { - ball.x = worldRight - inset; - ball.vx = -Math.abs(ball.vx); - } - if (ball.y < ceilingY + inset) { - ball.y = ceilingY + inset; - ball.vy = Math.abs(ball.vy); - } - const platformTop = platformY - ball.radius; - if (PLATFORM && ball.vy >= 0 && previous.y <= platformTop && ball.y >= platformTop && - ball.x >= platformLeft + ball.radius && - ball.x <= platformRight - ball.radius) { - ball.y = platformTop; - ball.vy = Math.abs(ball.vy) > 180 - ? -Math.abs(ball.vy) * (ball.bounce || .58) : 0; - ball.vx *= ball.drag || .992; - } else if (ball.y > floorY - ball.radius) { - ball.y = floorY - ball.radius; - ball.vy = Math.abs(ball.vy) > 180 - ? -Math.abs(ball.vy) * (ball.bounce || .62) : 0; - ball.vx *= ball.drag || .992; - } - const onSurface = ((PLATFORM && ball.x >= platformLeft + ball.radius && - ball.x <= platformRight - ball.radius && - Math.abs(ball.y - (platformY - ball.radius)) <= 2) || - ball.y >= floorY - ball.radius - 2) && Math.abs(ball.vy) < 180; - const poseTime = (now - startedAt) / 1000000; - const hitters = []; - for (const player of players) { - if (!player.alive || ((ball.safePlayers & (1 << player.pad)) && - now < ball.safeUntil)) - continue; - if (!player.attackHit && now < player.attackUntil) { - const strike = meleeStrike(player, now); - const distance = Math.hypot(ball.x - strike.x, ball.y - strike.y, - ball.z - strike.z); - if (distance <= ball.radius + strike.radius) - hitters.push({ player, contact: clamp(1 - distance / - (ball.radius + strike.radius), 0, 1) }); - } - } - if (hitters.length >= 2) { - crossWackBall(ball, hitters, now); - return; - } - if (hitters.length === 1) { - returnBall(ball, hitters[0].player, now, false, - .92 + hitters[0].contact * .38); - return; - } - for (const player of players) { - if (!player.alive || ((ball.safePlayers & (1 << player.pad)) && - now < ball.safeUntil)) - continue; - if (player.blocking) { - const shield = shieldGeometry(player); - const centerDistance = Math.hypot(ball.x - shield.x, ball.y - shield.y, - ball.z - shield.z); - const surfaceDistance = Math.max(0, centerDistance - ball.radius); - if (surfaceDistance <= shield.radius) { - const proximity = clamp(1 - surfaceDistance / shield.radius, 0, 1); - returnBall(ball, player, now, true, proximity); - return; - } - } - const geometry = runnerWorldGeometry(player, poseTime); - const currentHeadDistance = Math.max(0, Math.hypot( - ball.x - geometry.head.x, ball.y - geometry.head.y, - ball.z - geometry.head.z) - geometry.head.radius); - const sweptHeadDistance = Math.max(0, pointSegmentDistance( - geometry.head.x, geometry.head.y, geometry.head.z, - { x1: previous.x, y1: previous.y, z1: previous.z, - x2: ball.x, y2: ball.y, z2: ball.z }) - geometry.head.radius); - const headDistance = Math.min(currentHeadDistance, sweptHeadDistance); - const bodyContact = runnerContactToPoint(player, poseTime, - ball.x, ball.y, ball.z); - const bodyDistance = bodyContact.bodyDistance; - if (Math.min(headDistance, bodyDistance) > ball.radius) continue; - const runningContact = player.grounded && Math.abs(player.vx) > 40 && - (onSurface || ball.y >= player.y - ball.radius - 55); - if (runningContact) { - bootBall(ball, player, now); - return; - } - if (headDistance <= ball.radius) { - const sourcePad = ball.lastHitBy >= 0 && ball.lastHitBy !== player.pad - ? ball.lastHitBy : player.pad === 0 ? 1 : 0; - damageAllLimbs(player, ball.x - ball.vx, sourcePad, now); - bounceBallOffBody(ball, player, now, -1); - player.lastButton = "HEAD HIT"; - player.lastButtonAt = now; - return; - } - bounceBallOffBody(ball, player, now, bodyContact.segmentIndex); - return; - } -} - -// Returns whether the tap was spent on a double-tap move, so a single tap can -// still mean something else to the caller. -function directionTap(player, direction, now) { - const previousTap = player.lastTap[direction] || -10000000; - const releasedAt = player.lastRelease[direction] || -10000000; - player.lastTap[direction] = now; - if (now - previousTap > doubleTapUs || releasedAt <= previousTap || - now - releasedAt < doubleTapReleaseUs) return false; - player.lastTap[direction] = -10000000; - player.pendingMoveLabel = direction === "UP" ? "ULTRA JUMP" : "DASH " + direction; - playDrum("clap", 1.05, panPlayer(player)); - if (direction === "UP") { - player.vy = -ultraJumpVelocity; - player.grounded = false; - player.jumpHeld = false; - emitSignal("ultrajump", player.pad, 1, 0); - } else if (direction === "DOWN") { - // Grounded anywhere above the real floor and not riding a head means the - // platform, wherever it has been placed. Double-crouch sinks through it. - if (player.grounded && player.standingOn < 0 && player.y < floorY) - sink(player, now); - // Standing on the floor there is nothing under you to drop through and no - // height to convert, so the double-tap is spent rather than sold cheap. - else if (player.grounded) return true; - else { - // In the air this is a ground pound, and the height it starts from is - // the whole economy of the move — remember it now, because the crater - // is measured against where the fall actually began. - player.pounding = true; - player.poundFrom = player.y; - player.vy = Math.max(player.vy, poundLaunchVelocity); - player.grounded = false; - player.ducking = false; - player.pendingMoveLabel = "GROUND POUND"; - emitSignal("fastdrop", player.pad, 1, 0); - } - } else { - player.facing = direction === "RIGHT" ? 1 : -1; - player.dashVx = player.facing * 2400; - player.dashUntil = now + 110000; - emitSignal("dash", player.pad, player.facing, 0); - } - return true; -} - -function sink(player, now) { - player.sinkUntil = now + sinkDurationUs; - player.grounded = false; - player.jumpHeld = false; - player.vy = Math.max(player.vy, 260); - player.pendingMoveLabel = "SINK"; - playDrum("whoosh", .55, panPlayer(player)); - emitSignal("sink", player.pad, -1, 0); -} - -// A short low hop out of crouch. Crouching pins horizontal movement, so a -// direction flick from a tuck has nothing else to mean. -function crouchHop(player, direction, now) { - player.facing = direction; - player.vy = -crouchHopVelocity; - player.grounded = false; - player.jumpHeld = false; - player.hopUntil = now + crouchHopPoseUs; - player.pendingMoveLabel = "CROUCH HOP"; - playDrum("block", .58, panPlayer(player)); - emitSignal("crouchhop", player.pad, direction, 0); -} - -function killPlayer(target, killerPad, now, cause = "KO") { - if (!target.alive) return; - releaseCarriedBall(target, now); - freezeFinalFrame(now); - if (!deathCinematic && killerPad !== target.pad) - deathCinematic = { startedAt: now, loserPad: target.pad, - winnerPad: killerPad, cause }; - target.alive = false; - target.headBustedAt = now; - target.respawnAt = now + 1200000; - target.vx = 0; - target.vy = 0; - target.stance = "HIT"; - target.lastButton = cause; - target.lastButtonAt = now; - if (killerPad !== target.pad) players[killerPad].score += 1; - emitSignal(cause === "BALLED" ? "balled" : "ko", - killerPad, target.pad, players[killerPad]?.score || 0); - roundCause = cause; - impacts.push({ x: target.x, y: target.y - 120, z: target.z, life: .55, - duration: .55, death: true, explosion: false }); - playDrum("whoosh", 1.15, panPlayer(target)); - emitSignal("killcam", killerPad, target.pad, 1); - playDrum("snare", 1.15, panPlayer(target)); - // The dummy is the opponent everybody gets for free, and its fight is - // already running under the title screen — so its head is the one number - // the whole site can share without anybody signing in. Balling yourself - // does not count; somebody has to have popped it. - if (target.npc && !target.bot && killerPad !== target.pad) - emitSignal("dummy-popped", killerPad, target.pad, 1); -} - -// The crater. Everything about it is decided by the fall: a pound from a hop -// is a shove, a pound from the ceiling clears the stage. Whoever is standing -// inside the ring when it lands wears it; whoever is in the air over it has -// jumped it, which is the only defence and is meant to be readable from the -// silhouette alone. The ball in the ring is popped and re-served. -// -// The landing consumes the pounder's body but not their life. They continue as -// the existing controllable bouncing-head form, making the move a permanent -// mobility trade without prematurely ending the round. -function groundPound(player, now) { - player.pounding = false; - const fall = clamp(player.y - player.poundFrom, 0, poundFullFall); - const power = fall / poundFullFall; - const radius = poundMinRadius + (poundMaxRadius - poundMinRadius) * power; - const poseTime = (now - startedAt) / 1000000; - player.stance = "HIT"; - player.lastButton = "GROUND POUND"; - player.lastButtonAt = now; - impacts.push({ x: player.x, y: floorY, z: player.z, - life: .34, duration: .34, death: false, explosion: true }); - impactHitboxesUntil = Math.max(impactHitboxesUntil, now + 350000); - playDrum("kick", 1.3, panPlayer(player)); - emitSignal("blast", player.pad, player.x / worldRight, power); - - for (const target of players) { - if (target.pad === player.pad || !target.alive) continue; - const contact = runnerContactToPoint(target, poseTime, - player.x, floorY, player.z); - if (Math.min(contact.headDistance, contact.bodyDistance) > radius) continue; - if (!target.grounded) { - // Airborne over the ring is the dodge. It still throws you. - applyBodyHit(target, contact.segmentIndex, player.x, player.pad, now, - 900 + 700 * power, 620); - continue; - } - killPlayer(target, player.pad, now, "BLASTED"); - } - - for (const item of balls) { - if (!item.active) continue; - if (Math.hypot(item.x - player.x, item.y - floorY) > radius + item.radius) - continue; - item.active = false; - item.heldBy = -1; - item.vx = 0; - item.vy = 0; - item.serveAt = now + 1200000; - impacts.push({ x: item.x, y: item.y, z: item.z, - life: .22, duration: .22, death: false, explosion: false }); - emitSignal("ballblock", player.pad, 1, power); - } - - const geometry = runnerWorldGeometry(player, poseTime); - for (const limb of limbParts) - detachPart(player, limb, geometry, player.x, now); - detachPart(player, "torso", geometry, player.x, now); - player.alive = true; - player.stance = "BOUNCE"; - player.lastButton = "HEAD ONLY"; - player.lastButtonAt = now; -} - -function resolveMelee(now) { - const poseTime = (now - startedAt) / 1000000; - const contacts = []; - for (const attacker of players) { - if (!attacker.alive || attacker.attackHit || now >= attacker.attackUntil) continue; - const target = players[attacker.pad === 0 ? 1 : 0]; - if (!target.alive) continue; - const contact = meleeLimbContact(attacker, target, poseTime); - if (contact?.separation <= 3) { - attacker.attackHit = true; - contacts.push({ attacker, target, - strike: { x: contact.x, y: contact.y, z: contact.z }, - headshot: contact.headshot, segmentIndex: contact.segmentIndex }); - } - } - for (const { attacker, target, strike, headshot, segmentIndex } of contacts) { - if (!target.alive && contacts.length < 2) continue; - impacts.push({ x: strike.x, y: strike.y, z: strike.z, - life: .2, duration: .2, death: false, explosion: false }); - impactHitboxesUntil = Math.max(impactHitboxesUntil, now + 350000); - const away = Math.sign(target.x - attacker.x) || -attacker.facing; - const backBlocking = target.inputX === away; - if (target.blocking || backBlocking) { - target.stance = "DEFEND"; - target.blockFlash = 1; - target.lastButton = target.blocking ? "BLOCK" : "BACK BLOCK"; - target.lastButtonAt = now; - // An explicit shield returns the attacker's commitment as durable - // knockback. Keep it separate from controlled velocity so holding toward - // the defender cannot erase the recoil on the next simulation frame. - const recoil = target.blocking ? 2400 : 900; - attacker.dashUntil = 0; - attacker.dashVx = 0; - attacker.knockVx = -attacker.facing * recoil; - attacker.vx = attacker.knockVx + attacker.windVx; - if (target.blocking) attacker.vy = Math.min(attacker.vy, -415); - else target.vx = 0; - playDrum("block", 1.2, panPlayer(target)); - emitSignal("block", target.pad, attacker.pad, target.blocking ? 1 : 2); - // Only a real shield trades. A back-block still just shoves, because it - // costs nothing to hold a direction. - if (target.blocking) { - const spec = meleeSpecs[attacker.attackKind] || meleeSpecs.PUNCH; - const stun = shieldStunUs(spec.force * (attacker.attackMomentum || 1)); - attacker.hitStunUntil = Math.max(attacker.hitStunUntil, now + stun); - attacker.attackHit = true; - attacker.attackKind = ""; - attacker.attackUntil = 0; - attacker.stance = "STUN"; - breakShield(target, now); - } - } else if (headshot) killPlayer(target, attacker.pad, now, - contacts.length >= 2 ? "TRADE" : "KO"); - else { - const spec = meleeSpecs[attacker.attackKind] || meleeSpecs.PUNCH; - const momentum = attacker.attackMomentum || 1; - applyBodyHit(target, segmentIndex, attacker.x, attacker.pad, now, - spec.force * momentum, spec.lift * momentum); - playDrum(spec.thud[0], spec.thud[1], panPlayer(target)); - } - } -} - -function resolvePogoAttacks(now) { - const poseTime = (now - startedAt) / 1000000; - for (const attacker of players) { - if (!attacker.alive || !isPogo(attacker) || attacker.grounded || - attacker.pogoHit) continue; - const geometry = runnerWorldGeometry(attacker, poseTime); - const torso = geometry.segments.find((segment) => segment.role === "torso"); - if (!torso) continue; - const bottom = { x1: torso.x2, y1: torso.y2, z1: torso.z2, - x2: torso.x2, y2: torso.y2, z2: torso.z2, - width: torso.width + 6, role: "attack-pogo", part: "torso" }; - const target = players[attacker.pad === 0 ? 1 : 0]; - if (!target.alive) continue; - const contact = attackCapsuleContact([bottom], target, poseTime); - if (!contact || contact.separation > 3) continue; - attacker.pogoHit = true; - attacker.vy = Math.min(attacker.vy, -985); - attacker.lastButton = "POGO"; - attacker.lastButtonAt = now; - impacts.push({ x: contact.x, y: contact.y, z: contact.z, - life: .2, duration: .2, death: contact.headshot, explosion: false }); - if (contact.headshot) killPlayer(target, attacker.pad, now, "POGO"); - else applyBodyHit(target, contact.segmentIndex, attacker.x, - attacker.pad, now, 1350, 260); - playDrum("kick", 1.1, panPlayer(attacker)); - emitSignal("pogo", attacker.pad, contact.headshot ? 1 : 0, 0); - } -} - -function resolvePlayerPushboxes() { - if (!players[0].alive || !players[1].alive) return; - const poseTime = (runtime().monotonicUs - startedAt) / 1000000; - const firstBounds = runnerBounds(players[0], poseTime); - const secondBounds = runnerBounds(players[1], poseTime); - const verticalOverlap = Math.min(firstBounds.bottom, secondBounds.bottom) - - Math.max(firstBounds.top, secondBounds.top); - // Grounded fighters nudge one another. Once a jumper is clearly above the - // other fighter, the pushboxes separate so cross-over jumps are possible. - if (verticalOverlap <= 18 || - ((!players[0].grounded || !players[1].grounded) && - Math.abs(players[0].y - players[1].y) > 58)) return; - const left = players[0].x <= players[1].x ? players[0] : players[1]; - const right = left === players[0] ? players[1] : players[0]; - const pushRadius = (player) => isHeadOnly(player) ? 24 : isPogo(player) ? 38 : 69; - const minimumGap = pushRadius(left) + pushRadius(right); - const overlap = minimumGap - (right.x - left.x); - if (overlap <= 0) return; - const leftAdvancing = left.vx > 30; - const rightAdvancing = right.vx < -30; - const leftShare = leftAdvancing && !rightAdvancing ? .25 - : rightAdvancing && !leftAdvancing ? .75 : .5; - left.x -= overlap * leftShare; - right.x += overlap * (1 - leftShare); - if (leftAdvancing && !rightAdvancing) { - right.knockVx += Math.min(180, left.vx * .12); - left.vx *= .35; - } else if (rightAdvancing && !leftAdvancing) { - left.knockVx += Math.max(-180, right.vx * .12); - right.vx *= .35; - } else { - if (left.vx > 0) left.vx *= .35; - if (right.vx < 0) right.vx *= .35; - } - resolveRunnerBounds(left, 0); - resolveRunnerBounds(right, 0); -} - -function resolvePlayerStanding(now) { - const poseTime = (now - startedAt) / 1000000; - const previousStanding = players.map((player) => player.standingOn); - for (const player of players) player.standingOn = -1; - for (const rider of players) { - const base = players[rider.pad === 0 ? 1 : 0]; - if (!rider.alive || !base.alive || rider.vy < 0) continue; - const head = runnerWorldGeometry(base, poseTime).head; - const top = head.y - head.radius; - const horizontal = Math.abs(rider.x - head.x) <= head.radius + 42; - const depth = Math.abs(rider.z - head.z) <= head.radius + 32; - const crossed = (rider.previousY ?? rider.y) <= top + 8 && - rider.y >= top - 3; - const stayed = previousStanding[rider.pad] === base.pad && - rider.y <= top + 34; - if (!horizontal || !depth || (!crossed && !stayed)) continue; - const wasGrounded = rider.grounded; - rider.y = top; - rider.vy = Math.min(0, base.vy); - rider.grounded = true; - rider.standingOn = base.pad; - rider.sinkUntil = 0; - rider.hopUntil = 0; - if (!wasGrounded) rider.landPoseUntil = now + 110000; - } -} - -function updateStance(player, input, now) { - const opponent = players[player.pad === 0 ? 1 : 0]; - const toward = Math.sign(opponent.x - player.x) || player.facing || 1; - player.stance = !player.alive ? "HIT" - : now < player.hitStunUntil ? "STUN" - : player.blocking ? "DEFEND" - : player.heldBall >= 0 ? "HOLDING" - : player.grabHeld ? "REACHING" - : player.attackKind ? "ATTACK" - : now < player.sinkUntil ? "SINK" - : !player.grounded && now < player.hopUntil ? "CROUCH HOP" - : player.ducking ? player.grounded ? "CROUCH" : "AIR CROUCH" - : player.pogoDive ? "POGO DOWN" - : !player.grounded ? "AIR" - : now < player.dashUntil ? "DASH" - : input.horizontal === toward ? "ADVANCE" - : input.horizontal === -toward ? "RETREAT" - : "NEUTRAL"; -} - -function grabNearestBall(player, now) { - let nearest = null; - let nearestDistance = 240; - for (let index = 0; index < balls.length; index++) { - const item = balls[index]; - if (!item.active || item.heldBy >= 0) continue; - const distance = Math.hypot(item.x - player.x, - item.y - (player.y - 82), item.z - player.z); - if (distance >= nearestDistance) continue; - nearest = { item, index }; - nearestDistance = distance; - } - if (!nearest) return false; - player.heldBall = nearest.index; - nearest.item.heldBy = player.pad; - nearest.item.safeUntil = now + 180000; - nearest.item.safePlayers = 1 << player.pad; - player.lastButton = "HOLDING"; - player.lastButtonAt = now; - emitSignal("grab", player.pad, nearest.index, nearest.item.mass); - playDrum("clap", .72, panPlayer(player)); - return true; -} - -function releaseCarriedBall(player, now) { - if (player.heldBall < 0) return; - const item = balls[player.heldBall]; - if (item) { - item.heldBy = -1; - item.x = player.x + player.facing * (item.radius + 70); - item.y = player.y - 88; - item.z = player.z; - item.vx = player.vx; - item.vy = player.vy; - item.safeUntil = now + 120000; - item.safePlayers = 1 << player.pad; - emitSignal("release", player.pad, player.heldBall, item.mass); - } - player.heldBall = -1; -} - -function bouncePogoOnSurface(player, surfaceY, now) { - player.y = surfaceY; - player.vy = -pogoBounceVelocity; - player.grounded = false; - player.pogoDive = false; - player.pogoHit = false; - player.stance = "POGO BOUNCE"; - player.lastButton = "POGO BOUNCE"; - player.lastButtonAt = now; - playDrum("kick", .9, panPlayer(player)); - emitSignal("pogo", player.pad, -2, pogoBounceVelocity); -} - -function updatePlayer(player, pad, dt, now) { - player.previousY = player.y; - if (!player.alive) { - player.previous = pad.down.slice(); - if (now >= player.respawnAt) { - player.x = player.spawnX; - player.y = floorY; - player.z = 0; - player.vx = 0; - player.vy = 0; - player.vz = 0; - player.windVx = 0; - player.knockVx = 0; - player.grounded = true; - player.ducking = false; - player.inputX = 0; - player.inputY = 0; - player.dashUntil = 0; - player.stance = "NEUTRAL"; - player.crouchBlend = 0; - player.jumpLaunchAt = 0; - player.jumpPoseUntil = 0; - player.landPoseUntil = 0; - player.jumpHeld = false; - player.hopUntil = 0; - player.sinkUntil = 0; - player.crouchJump = false; - player.attackMomentum = 1; - player.hitSegment = -1; - player.hitSegmentUntil = 0; - player.hitStunUntil = 0; - player.alive = true; - } - return; - } - player.suppressedDirections = player.suppressedDirections.filter((button) => - pad.down.includes(button)); - const headOnly = isHeadOnly(player); - const pogo = isPogo(player); - const legCount = ["left-leg", "right-leg"] - .filter((part) => hasPart(player, part)).length; - const armCount = ["left-arm", "right-arm"] - .filter((part) => hasPart(player, part)).length; - const rawInput = quantizedInput(pad, player.suppressedDirections); - const hitStunned = now < player.hitStunUntil; - const wasBlocking = player.blocking; - // A broken shield stays down until X is let go, so the opening it bought is - // spent on attacking rather than on re-guarding by reflex. - if (player.shieldLocked && !pad.down.includes("X")) player.shieldLocked = false; - player.blocking = !headOnly && pad.down.includes("X") && !player.shieldLocked; - if (player.blocking && !wasBlocking) { - player.shieldVx = player.vx - player.windVx - player.knockVx; - player.dashUntil = 0; - player.dashVx = 0; - player.lastTap = {}; - player.lastRelease = {}; - } - const input = player.blocking ? { horizontal: 0, vertical: 0 } : rawInput; - const grabHeld = armCount > 0 && !pogo && !hitStunned && !player.blocking && - pad.down.includes("A") && pad.down.includes("B"); - if (grabHeld && !player.grabHeld && player.heldBall < 0) { - if (!grabNearestBall(player, now)) { - player.lastButton = "REACHING"; - player.lastButtonAt = now; - emitSignal("reach", player.pad, player.facing, 0); - } - } - else if (!grabHeld && player.heldBall >= 0) - releaseCarriedBall(player, now); - const inputChanged = input.horizontal !== player.inputX || - input.vertical !== player.inputY; - if (inputChanged && - (input.horizontal || input.vertical)) - emitSignal("move", player.pad, input.horizontal, input.vertical); - player.pendingMoveLabel = ""; - const upPressed = input.vertical > 0 && !player.previous.includes("MOVE_UP"); - const downPressed = input.vertical < 0 && player.inputY >= 0; - const wasCrouched = player.ducking || player.crouchBlend >= .35; - const crouchTarget = input.vertical < 0 || - (!player.grounded && player.crouchJump && now < player.jumpPoseUntil) || - (!player.grounded && now < player.hopUntil) ? 1 : 0; - const crouchStep = dt * (crouchTarget ? 9 : 11); - player.crouchBlend += clamp(crouchTarget - player.crouchBlend, - -crouchStep, crouchStep); - player.ducking = player.crouchBlend >= .52; - if (player.attackKind && now >= player.attackUntil) { - player.attackKind = ""; - player.attackHit = false; - player.attackMomentum = 1; - } - if (player.itemAction && now >= player.itemActionUntil) - player.itemAction = ""; - if (player.inputX && input.horizontal !== player.inputX) - player.lastRelease[player.inputX > 0 ? "RIGHT" : "LEFT"] = now; - if (player.inputY && input.vertical !== player.inputY) - player.lastRelease[player.inputY > 0 ? "UP" : "DOWN"] = now; - if (input.horizontal && input.horizontal !== player.inputX) { - const direction = input.horizontal > 0 ? "RIGHT" : "LEFT"; - recordCommand(player, direction, now); - if (!directionTap(player, direction, now) && player.grounded && - player.ducking && !headOnly && !pogo && !hitStunned) - crouchHop(player, input.horizontal, now); - } - if (input.vertical && input.vertical !== player.inputY) { - const direction = input.vertical > 0 ? "UP" : "DOWN"; - recordCommand(player, direction, now); - directionTap(player, direction, now); - if (headOnly) { - const alternating = player.headPumpDirection && - player.headPumpDirection !== input.vertical && - now - (player.headPumpAt || 0) <= 520000; - player.headBounceCharge = clamp((player.headBounceCharge || 0) + - (alternating ? .2 : .035), 0, 1); - player.headPumpDirection = input.vertical; - player.headPumpAt = now; - if (alternating && player.grounded) { - player.vy = -lerp(420, headBounceVelocity * 1.7, - player.headBounceCharge); - player.grounded = false; - player.stance = "BOUNCE " + Math.round(player.headBounceCharge * 5); - playDrum("block", .45 + player.headBounceCharge * .45, - panPlayer(player)); - emitSignal("head-bounce", player.pad, input.vertical, - player.headBounceCharge); - } - } - } - - if (headOnly) { - const sincePump = now - (player.headPumpAt || 0); - if (sincePump > 650000) - player.headBounceCharge = Math.max(0, - (player.headBounceCharge || 0) - dt * .22); - } - - if (input.horizontal) player.facing = input.horizontal; - // Fighting-game directions are digital: full movement begins and ends on - // the sampled edge. The analog stick is only an eight-way gate. - if (player.grounded) player.windVx *= Math.max(0, 1 - dt * 10); - else player.windVx = clamp(player.windVx + windAcceleration * dt, -900, 900); - player.knockVx *= Math.max(0, 1 - dt * (player.grounded ? 7 : 1.8)); - if (now < player.dashUntil && input.horizontal && - Math.sign(player.dashVx) !== input.horizontal) { - player.dashUntil = 0; - player.dashVx = 0; - player.runSince = 0; - } - const dashRunningOut = player.grounded && input.horizontal && - player.dashUntil > 0 && now >= player.dashUntil; - if (dashRunningOut && !player.runSince) { - player.runSince = now; - } - if (!player.grounded || !input.horizontal || player.blocking || - player.ducking || hitStunned) player.runSince = 0; - const headAirControl = player.grounded ? .55 - : lerp(.55, .9, player.headBounceCharge || 0); - const mobility = headOnly ? headAirControl - : pogo ? .68 : legCount === 1 ? .72 : 1; - const runSpeed = player.runSince - ? Math.min(runTopSpeed, runStartSpeed + - (now - player.runSince) / 1000000 * runAcceleration) : 0; - const controlledVx = player.blocking ? player.shieldVx || 0 - : now < player.dashUntil && Math.abs(player.dashVx) > 0 - ? player.dashVx - : player.runSince ? input.horizontal * runSpeed * mobility - : player.ducking && player.grounded ? 0 - : input.horizontal * walkSpeed * mobility; - player.vx = controlledVx + player.windVx + player.knockVx; - if (inputChanged) telemetry("FIGHT_MOVE", player.name + - " pad=" + (player.pad + 1) + - " held=" + (pad.down.filter((button) => button.startsWith("Arrow")).join("+") || "NONE") + - " stick=" + pad.leftX.toFixed(2) + "," + pad.leftY.toFixed(2) + - " quant=" + input.horizontal + "," + input.vertical + - " controlled=" + Math.round(controlledVx) + - " shield=" + (player.blocking ? 1 : 0) + - " wind=" + Math.round(player.windVx) + - " knock=" + Math.round(player.knockVx) + - " vx=" + Math.round(player.vx) + - " x=" + Math.round(player.x)); - - // Checked before liftoff so the launch frame itself can never be cut. - if (player.jumpHeld && player.vy < 0 && !player.blocking && - input.vertical <= 0) { - player.vy *= jumpCutScale; - player.jumpHeld = false; - } - if (!headOnly && upPressed && player.grounded && !player.jumpLaunchAt) { - player.jumpLaunchAt = now + jumpAnticipationUs; - player.crouchJump = wasCrouched; - player.pendingMoveLabel = wasCrouched ? "CROUCH JUMP" : "JUMP"; - } - if (player.jumpLaunchAt && now >= player.jumpLaunchAt) { - player.jumpLaunchAt = 0; - player.jumpPoseUntil = now + - (player.crouchJump ? crouchJumpPoseUs : jumpPoseUs); - const jumpScale = pogo ? .88 : legCount === 1 ? .78 : 1; - player.vy = Math.min(player.vy, - -(player.crouchJump ? crouchJumpVelocity : jumpVelocity) * jumpScale); - player.jumpHeld = true; - player.pogoHit = false; - player.pogoDive = false; - player.pounding = false; - player.grounded = false; - player.ducking = player.crouchJump; - playDrum("block", 0.72, panPlayer(player)); - emitSignal("jump", player.pad, 1, 0); - } - if (pogo && !player.grounded && !hitStunned && downPressed) { - player.vy = Math.max(player.vy, 2250); - player.pogoDive = true; - player.pogoHit = false; - player.pendingMoveLabel = "POGO DOWN"; - player.stance = "POGO DOWN"; - playDrum("kick", .78, panPlayer(player)); - emitSignal("pogo", player.pad, -1, player.vy); - } - if (!player.grounded && !pogo && input.vertical < 0) { - player.ducking = true; - player.pendingMoveLabel ||= "AIR CROUCH"; - } - - // Y and either shoulder spend an item, so reaching for a kick can no - // longer empty a magazine. A loaded hand still colors the punch — into a - // swing, never a shot. - const acting = !headOnly && !pogo && !hitStunned && !player.blocking; - for (const button of pad.down) { - if (!player.previous.includes(button)) { - remember(player, button); - if (button === "X" && !headOnly) { - player.pendingMoveLabel = "SHIELD"; - playDrum("block", .7, panPlayer(player)); - emitSignal("shield", player.pad, 1, 0); - } - else if (acting && !grabHeld && button === "A") - startMelee(player, "KICK", now); - else if (acting && !grabHeld && button === "B") - startMelee(player, itemMelee[heldItem(player)] || "PUNCH", now); - else if ((acting || (headOnly && !hitStunned)) && - ["Y", "LeftShoulder", "RightShoulder"].includes(button)) { - const item = heldItem(player); - if (item === "GUN") fireGun(player, input); - else if (item === "GRENADE") throwGrenade(player); - } - } - } - if (grabHeld) { - player.lastButton = player.heldBall >= 0 ? "HOLDING" : "REACHING"; - player.lastButtonAt = now; - } - if (player.pendingMoveLabel) remember(player, player.pendingMoveLabel); - - const previousY = player.y; - const wasGrounded = player.grounded; - player.vy += (player.vy < 0 ? riseGravity : fallGravity) * dt; - // Keeping DOWN held through a pound drives it down harder. The button is - // the only thing that makes a pound faster, so the height you fell from and - // the pressure you kept on it are the two dials on the crater. - if (player.pounding && input.vertical < 0) - player.vy = Math.min(poundMaxVelocity, - player.vy + poundHoldAcceleration * dt); - player.x += player.vx * dt; - player.y += player.vy * dt; - player.grounded = false; - // A sinking fighter is transparent to the platform but never to the floor. - if (PLATFORM && now >= player.sinkUntil && - player.vy >= 0 && previousY <= platformY && player.y >= platformY && - player.x >= platformLeft && player.x <= platformRight) { - player.y = platformY; - if (headOnly) { - player.vy = 0; - player.grounded = true; - player.stance = input.horizontal ? "ROLL" : "HEAD ONLY"; - } else if (pogo && player.pogoDive) { - bouncePogoOnSurface(player, platformY, now); - } else { - player.vy = 0; - player.grounded = true; - } - } else if (player.y >= floorY) { - player.y = floorY; - if (headOnly) { - player.vy = 0; - player.grounded = true; - player.stance = input.horizontal ? "ROLL" : "HEAD ONLY"; - } else if (pogo && player.pogoDive) { - bouncePogoOnSurface(player, floorY, now); - } else { - player.vy = 0; - player.grounded = true; - } - } - if (!wasGrounded && player.grounded) { - player.landPoseUntil = now + 110000; - player.crouchJump = false; - player.jumpHeld = false; - player.hopUntil = 0; - player.sinkUntil = 0; - if (player.pounding) groundPound(player, now); - } - resolveRunnerBounds(player, (now - startedAt) / 1000000); - player.hit = Math.max(0, player.hit - dt * 4); - player.blockFlash = Math.max(0, player.blockFlash - dt * 6); - player.previous = pad.down.slice(); - if (input.vertical > 0) player.previous.push("MOVE_UP"); - player.inputX = input.horizontal; - player.inputY = input.vertical; - player.grabHeld = grabHeld; - updateStance(player, input, now); -} - -// How long a bot leans on a button, per action rather than one house number. -// A jump has to outlast the whole rise (1760 / 4800 ≈ .37s) or jumpCutScale -// clips the arc to a third of its height and the platform goes out of reach; -// a strike is a stab; a sink tap is half of a double-tap. -const botHoldUs = { - walk: 90000, jump: 440000, sinkTap: 60000, - strike: 70000, shield: 120000, item: 110000, -}; -// A press cannot reopen until it has been up this long, so `lastRelease` -// always sees a real gap — that gap is what keeps dash, ultra jump and sink -// readable from synthetic input. Four frames clears doubleTapReleaseUs even -// when the release lands a frame late. -const botRestUs = 66668; -// A thumb cannot hold both ends of an axis, so opening one closes the other. -const botAxis = { ArrowLeft: "ArrowRight", ArrowRight: "ArrowLeft", - ArrowUp: "ArrowDown", ArrowDown: "ArrowUp" }; - -// The bot's dice. Not Math.random, deliberately: two fresh sims must roll -// identically (the determinism test holds that line), and the reel factory -// reserves the global stream for the match name. Seeded per fighter at round -// start from the round clock, so rounds differ live while a replayed sim -// replays exactly. -function botRoll(player) { - let s = player.botRngState = (player.botRngState + 0x6d2b79f5) >>> 0; - s = Math.imul(s ^ (s >>> 15), s | 1); - s ^= s + Math.imul(s ^ (s >>> 7), s | 61); - return ((s ^ (s >>> 14)) >>> 0) / 4294967296; -} - -// The one door every synthetic press goes through. Pressing a button that is -// already down extends it, so a pursuit reads as one lean instead of a -// stutter; pressing one that is still resting is refused. -function botPress(player, button, holdUs, now) { - const press = player.botPresses[button]; - if (press && now < press.until) { - press.until = Math.max(press.until, now + holdUs); - return true; - } - if (press && now < press.until + botRestUs) return false; - const other = player.botPresses[botAxis[button]]; - if (other && now < other.until) other.until = now; - player.botPresses[button] = { until: now + holdUs }; - return true; -} - -const botHeld = (player, button, now) => - now < (player.botPresses[button]?.until || 0); - -function botDown(player, now) { - return Object.keys(player.botPresses) - .filter((button) => now < player.botPresses[button].until); -} - -function botPad(player, opponent, now) { - if (!player.bot) { - player.botPresses = {}; - return { connected: true, down: [], leftX: 0, leftY: 0 }; - } - // Downed, or standing over a downed opponent, the bot opens nothing new — - // but presses already out still run their length instead of vanishing - // mid-hold, which would read as a poke. - if (!player.alive || !opponent.alive) - return { connected: true, down: botDown(player, now), leftX: 0, leftY: 0 }; - - // A scored bot plays its sheet instead of thinking: rows of {at, button, - // hold} in µs from round start, through the same botPress door so holds - // and releases stay human-shaped. An empty score is a dummy that stands - // there and takes it — which is exactly what a sync lab needs on the - // receiving end. Scores arrive via globalThis.__oskiewarBotScores at round - // start; no score, and the brain below plays as always. - if (player.botScore) { - const at = now - roundStartedAt; - while (player.botScoreAt < player.botScore.length && - player.botScore[player.botScoreAt].at <= at) { - const row = player.botScore[player.botScoreAt]; - if (botPress(player, row.button, row.hold ?? botHoldUs.strike, now)) - player.botScoreAt += 1; - else break; - } - return { connected: true, down: botDown(player, now), leftX: 0, leftY: 0 }; - } - - const dx = opponent.x - player.x; - const distance = Math.abs(dx); - const toward = Math.sign(dx) || player.facing || -1; - const opponentThreatening = opponent.attackKind && - now < opponent.attackUntil && distance < 245 && - opponent.facing === -toward; - const striking = botHeld(player, "A", now) || botHeld(player, "X", now); - - // A guard that answers every swing the frame it starts turns the round into - // punch-into-shield until the clock dies — the signal census reads punch×6, - // shield×6 per five seconds and nothing else. So the guard is human now: - // each incoming swing is noticed once, answered late, and sometimes not at - // all. The lapse is what lets damage through, and damage is what ends - // rounds. - if (opponentThreatening) { - if (player.botThreatSeen !== opponent.attackStartedAt) { - player.botThreatSeen = opponent.attackStartedAt; - const roll = botRoll(player); - player.botShieldAt = roll < 0.55 - ? now + 70000 + Math.round(roll * 250000) : Infinity; - } - } else player.botShieldAt = Infinity; - - // A shield that just ate a swing bought a stun. Swing into it now — the - // neutral-game cooldown would let the opening close unanswered. - if (player.shieldBrokenAt && - player.shieldBrokenAt !== player.botPunishedAt && - now - player.shieldBrokenAt < 80000) { - player.botPunishedAt = player.shieldBrokenAt; - player.botAttackAt = now; - } - - if (opponentThreatening && !striking && now >= player.botShieldAt) - botPress(player, "X", botHoldUs.shield, now); - else { - // The old spacing was tuned for a bigger fighter: stop walking at 155, - // swing at 225. A punch capsule tops out near 130 of separation and a - // kick near 170, so that band was whiff purgatory — the census read - // punch after punch with no body hit until the clock died. Close to - // where a swing actually lands, and swing only from there — and only on - // the ground: steering mid-air wedged the bot against a platform lip it - // used to clear. - if (player.grounded && distance > 95) - botPress(player, toward > 0 ? "ArrowRight" : "ArrowLeft", - botHoldUs.walk, now); - // Items are ranged, so the bot spends them while closing rather than - // trading one for a swing it could land anyway. The grenade needs its arc. - const item = heldItem(player); - if (item && now >= player.botItemAt && - distance >= (item === "GUN" ? 320 : 1200) && - botPress(player, "Y", botHoldUs.item, now)) - player.botItemAt = now + 600000; - if (distance < 165 && now >= player.botAttackAt && - botPress(player, player.botAttackSequence % 2 ? "A" : "B", - botHoldUs.strike, now)) { - player.botAttackSequence += 1; - player.botAttackAt = now + 330000 + (player.botAttackSequence % 3) * 70000; - } - } - - if (player.grounded && opponent.y < player.y - 180 && - now >= player.botJumpAt && - botPress(player, "ArrowUp", botHoldUs.jump, now)) - player.botJumpAt = now + 1250000; - // Knocked up onto the platform the bot would camp out of reach, so it plays - // the same double-tap-down a player would: two real presses with a real - // release between them. - if (PLATFORM && player.grounded && player.standingOn < 0 && - player.y < floorY - 40 && - opponent.y > player.y + 200 && now >= player.botSinkAt) { - player.botSinkAt = now + 900000; - player.botSinkTaps = 2; - player.botSinkNextAt = now; - } - if (player.botSinkTaps > 0 && now >= player.botSinkNextAt && - botPress(player, "ArrowDown", botHoldUs.sinkTap, now)) { - player.botSinkTaps -= 1; - player.botSinkNextAt = now + botHoldUs.sinkTap + botRestUs; - } - return { connected: true, down: botDown(player, now), leftX: 0, leftY: 0 }; -} - -function gameSim() { - syncGameView(); - const now = runtime().monotonicUs; - const dt = Math.min(0.04, Math.max(0.001, (now - lastSimAt) / 1000000)); - lastSimAt = now; - simulateAirParticles(dt, now); - if (roundViewer) { - updateRoundViewer(now, dt); - return; - } - padSnapshots[0] = gamepad(0); - padSnapshots[1] = gamepad(1); - inputPads[0] = padSnapshots[0]; - inputPads[1] = padSnapshots[1]; - if (!selecting && Array.isArray(globalThis.__oskiewarTouch?.taps)) - globalThis.__oskiewarTouch.taps.length = 0; - if (consumeSystemButtons(now)) return; - // The wordmark screen is a live training round, so the shell reads start - // and then falls straight through into the fight it is sitting on top of. - if (shellMode === "MENU") updateShell(now); - for (const player of players) - inputPads[player.pad] = player.bot && shellMode === "GAME" - ? botPad(player, players[player.pad ? 0 : 1], now) - : player.npc ? { connected: true, down: [], leftX: 0, leftY: 0 } - : padSnapshots[player.pad]; - if (debugHitboxes && now >= nextInputDebugAt) { - nextInputDebugAt = now + 500000; - const values = players.map((player) => { - const pad = inputPads[player.pad]; - const input = quantizedInput(pad, player.suppressedDirections); - return "P" + (player.pad + 1) + " down=" + - (pad.down.join("+") || "NONE") + " stick=" + - pad.leftX.toFixed(2) + "," + pad.leftY.toFixed(2) + " q=" + - input.horizontal + "," + input.vertical + " vx=" + - Math.round(player.vx); - }); - telemetry("FIGHT_INPUT", values.join(" | ")); - } - recordReplayCommands(now, inputPads); - publishSpectator(now); - if (roundResult) { - // The scored tail. The killcam dwell used to be the reel's quietest - // seconds under its most dramatic frames, so the dwell keeps a slow - // heartbeat and the result card lands with a sting of its own. Voice on - // the drum channel, record on unrouted signal names, as everywhere. - if (!resultPulseAt) resultPulseAt = roundOverAt + 900000; - if (now >= resultPulseAt) { - resultPulseAt = now + 1050000; - playDrum("kick", .3, 0); - emitSignal("result-pulse", -1, 0, 0); - } - if (!resultCardStung && now - roundOverAt >= 1100000) { - resultCardStung = true; - playDrum("bell", .95, 0); - emitSignal("result-card", -1, roundResult === "TIE" ? 0 : 1, 0); - } - if (INSTANT_REPLAY && instantReplay) { - updateInstantReplay(now, dt); - return; - } - if (INSTANT_REPLAY) { - const replayDown = padSnapshots[0]?.down || []; - if (replayDown.includes("Y") && !replayOfferPrevious.includes("Y") && - startInstantReplay(now)) return; - replayOfferPrevious = replayDown.slice(); - } - updateCameraDoll(dt, now); - captureFrameTelemetry(now); - const resultDuration = matchOver ? matchResultUs : roundResultUs; - if (now - roundOverAt >= resultDuration) { - if (selfPlay) startSelfPlay(now); - else returnToTitle(now, "round-end"); - } - return; - } - if (PAL_SELECT && selecting) { - updateSelect(now); - return; - } - if (now - roundStartedAt < introDurationUs) { - // The intro used to pass in silence — a reel that opens on the countdown - // opened on three mute seconds, and a player heard the round begin with - // nothing. Ring the "3, 2, 1" on the same two channels the round clock - // uses: the drum for the ear, the signal for the record. - const introSecond = Math.ceil( - (introDurationUs - (now - roundStartedAt)) / 1000000); - if (introSecond !== lastIntroSecond) { - lastIntroSecond = introSecond; - // playDrum is the voice, the signal is the record — the "countdown" - // name is deliberately not in the sfx routes, or the web would voice - // every bell twice. value2 marks these as the intro's. - playDrum("bell", .72 + (3 - introSecond) * .12, 0); - emitSignal("countdown", -1, introSecond, 1); - } - updateCameraDoll(dt, now); - captureFrameTelemetry(now); - return; - } - if (lastIntroSecond > 0) { - // The first live tick after the intro is the round opening — one accent, - // so "go" is a sound and not just a vanished number. - lastIntroSecond = -1; - playDrum("block", 1.1, 0); - emitSignal("fighters-lock", -1, 0, 0); - } - roundElapsedUs += dt * 1000000; - const timedRound = roundIsTimed(); - if (timedRound) { - const countdownSecond = Math.max(0, - Math.ceil((roundDurationUs - roundElapsedUs) / 1000000)); - if (countdownSecond > 0 && countdownSecond <= 10 && - countdownSecond !== lastCountdownSecond) { - lastCountdownSecond = countdownSecond; - playDrum("bell", 1 + (10 - countdownSecond) * .025, 0); - emitSignal("countdown", -1, countdownSecond, 0); - } - } - updateWind(dt, now); - updatePlayer(players[0], inputPads[0], dt, now); - updatePlayer(players[1], inputPads[1], dt, now); - resolvePlayerStanding(now); - resolvePlayerPushboxes(); - updatePowerups(now); - updateBodyTrees(dt, now); - updateBullets(dt, now); - updateGrenades(dt, now); - resolveMelee(now); - resolvePogoAttacks(now); - for (const item of balls) updateBall(item, dt, now); - updateDetachedParts(dt); - updateCamera(dt); - updateCameraDoll(dt, now); - captureFrameTelemetry(now); - captureRoundReplay(now); - recordReplayCheckpoint(now); - for (const impact of impacts) impact.life -= dt; - while (impacts.length && impacts[0].life <= 0) impacts.shift(); - if (players.some((player) => !player.alive) || - (timedRound && roundElapsedUs >= roundDurationUs)) { - if (timedRound && roundElapsedUs >= roundDurationUs && - players.every((player) => player.alive)) - roundCause = "TIME"; - finishRound(now); - } -} - -function circle(x, y, radius, width, color) { - let lastX = x + radius; - let lastY = y; - for (let i = 1; i <= 12; i++) { - const angle = i * Math.PI * 2 / 12; - const nextX = x + Math.cos(angle) * radius; - const nextY = y + Math.sin(angle) * radius; - line(lastX, lastY, nextX, nextY, width, ...color); - lastX = nextX; - lastY = nextY; - } -} - -// Discs are the piece's bulk geometry — every capsule end, every joint. The -// unit ring is constant, so the trig runs once at load, and fanning from a rim -// vertex covers the identical decagon in 8 faces instead of 10 from the center -// (cull is off, depth is flat, colors are flat — the rasterized pixels match). -// A silhouette only has to be as fine as its size on screen. A limb cap a few -// pixels across reads round at six sides; a head at ninety does not, and used -// to show its corners. Rings are precomputed per step so no trig runs inside a -// frame, and the small end is cheaper than the one fixed ring it replaces. -const discRings = [6, 8, 12, 16, 24, 32].map((sides) => { - const ring = []; - for (let side = 0; side < sides; side++) { - const a = side * Math.PI * 2 / sides; - ring.push(Math.cos(a), Math.sin(a)); - } - return ring; -}); -const discRingFor = (radius) => discRings[ - radius < 6 ? 0 : radius < 13 ? 1 : radius < 26 ? 2 - : radius < 52 ? 3 : radius < 110 ? 4 : 5]; - -function filledDisc(x, y, radius, color) { - const [r, g, b] = color; - const ring = discRingFor(radius); - const originX = x + ring[0] * radius, originY = y + ring[1] * radius; - let lastX = x + ring[2] * radius, lastY = y + ring[3] * radius; - for (let side = 4; side < ring.length; side += 2) { - const nextX = x + ring[side] * radius; - const nextY = y + ring[side + 1] * radius; - screenTriangle(originX, originY, lastX, lastY, nextX, nextY, r, g, b); - lastX = nextX; - lastY = nextY; - } -} - -function filledRing(x, y, outerRadius, innerRadius, color) { - const [r, g, b] = color; - const sides = 14; - const inner = Math.max(0, Math.min(outerRadius, innerRadius)); - for (let side = 0; side < sides; side++) { - const a = side * Math.PI * 2 / sides; - const b = (side + 1) * Math.PI * 2 / sides; - const outerA = { x: x + Math.cos(a) * outerRadius, - y: y + Math.sin(a) * outerRadius }; - const outerB = { x: x + Math.cos(b) * outerRadius, - y: y + Math.sin(b) * outerRadius }; - const innerA = { x: x + Math.cos(a) * inner, - y: y + Math.sin(a) * inner }; - const innerB = { x: x + Math.cos(b) * inner, - y: y + Math.sin(b) * inner }; - screenTriangle(outerA.x, outerA.y, innerA.x, innerA.y, - outerB.x, outerB.y, r, g, b); - screenTriangle(innerA.x, innerA.y, innerB.x, innerB.y, - outerB.x, outerB.y, r, g, b); - } -} - -function filledCapsule(x1, y1, x2, y2, width, color) { - const [r, g, b] = color; - const dx = x2 - x1; - const dy = y2 - y1; - const length = Math.hypot(dx, dy); - const radius = width / 2; - if (length < .001) { - filledDisc(x1, y1, radius, color); - return; - } - const nx = -dy / length * radius; - const ny = dx / length * radius; - screenTriangle(x1 + nx, y1 + ny, x1 - nx, y1 - ny, - x2 + nx, y2 + ny, r, g, b); - screenTriangle(x1 - nx, y1 - ny, x2 - nx, y2 - ny, - x2 + nx, y2 + ny, r, g, b); - filledDisc(x1, y1, radius, color); - filledDisc(x2, y2, radius, color); -} - -// Xbox batches GPU triangles above its D2D line layer, so every bone and joint -// must share this triangle path. The wider silhouette pass and color pass form -// conventional rounded capsules without the native renderer reordering them. -function damagedPartColor(color, player, part) { - const damage = Number(player?.partDamage?.[part] || 0); - if (!damage) return color; - const durability = part === "torso" ? 3 : 2; - const amount = clamp(damage / durability * .92, 0, .92); - return mixColor(color, [244, 34, 50], amount); -} - -function drawSkeletonSegments(segments, color, outline, player = null) { - const edge = Math.max(1.25, Math.min(3, cameraScale() * 1.8)); - for (const segment of segments) - filledCapsule(segment.x1, segment.y1, segment.x2, segment.y2, - segment.width + edge * 2, outline); - for (const segment of segments) - filledCapsule(segment.x1, segment.y1, segment.x2, segment.y2, - segment.width, damagedPartColor(color, player, segment.part)); -} - -function drawFighterSilhouette(geometry, color, outline, player = null) { - drawSkeletonSegments(geometry.segments, color, outline, player); - const headEdge = Math.max(1.25, Math.min(3, cameraScale() * 1.8)); - // The neck connector and solid head are emitted into the same triangle - // silhouette pass, so the head cannot detach as a separate line-layer ring. - filledDisc(geometry.head.x, geometry.head.y, - geometry.head.radius + headEdge, outline); - filledDisc(geometry.head.x, geometry.head.y, geometry.head.radius, color); -} - -function fighterAnimationPhase(player, now = null) { - const run = runtime(); - now ??= run.simMonotonicUs || run.monotonicUs; - let state = "IDLE"; - let stateStartedAt = startedAt; - if (!player.alive) { - state = "KO"; - stateStartedAt = player.headBustedAt || player.lastButtonAt || now; - } else if (now < player.hitStunUntil) { - state = "HIT"; - stateStartedAt = player.lastButtonAt || now; - } else if (player.blocking) { - state = "SHIELD"; - stateStartedAt = player.lastButtonAt || now; - } else if (player.heldBall >= 0) { - state = "HOLD"; - stateStartedAt = player.lastButtonAt || now; - } else if (player.grabHeld) { - state = "REACH"; - stateStartedAt = player.lastButtonAt || now; - } else if (player.itemAction && now < player.itemActionUntil) { - state = player.itemAction; - stateStartedAt = player.itemActionStartedAt || now; - } else if (player.attackKind && now < player.attackUntil) { - state = player.attackKind; - stateStartedAt = player.attackStartedAt || now; - } else if (isPogo(player)) { - state = "POGO"; - } else if (now < player.sinkUntil) { - state = "SINK"; - stateStartedAt = player.sinkUntil - sinkDurationUs; - } else if (!player.grounded && now < player.hopUntil) { - state = "CROUCH HOP"; - stateStartedAt = player.hopUntil - crouchHopPoseUs; - } else if (player.ducking) { - state = player.grounded ? "CROUCH" : "AIR CROUCH"; - } else if (!player.grounded) { - state = player.vy < 0 ? "JUMP" : "FALL"; - stateStartedAt = player.jumpPoseUntil - ? player.jumpPoseUntil - (player.crouchJump ? crouchJumpPoseUs : jumpPoseUs) - : player.lastButtonAt || now; - } else if (shellMode === "MENU" && titleAttractMode === "still") { - state = "MEDITATE"; - } else if (now < player.dashUntil) { - state = "DASH"; - stateStartedAt = player.lastButtonAt || now; - } else if (player.runSince) { - state = "RUN"; - stateStartedAt = player.runSince; - } else if (Math.abs(player.vx) > 40) { - state = "WALK"; - } - const [steps, ticksPerStep, basePhase, loop] = - fighterAnimationSpecs[state] || fighterAnimationSpecs.IDLE; - const rawTick = Math.max(0, - Math.floor((now - stateStartedAt) / replayTickUs)); - const rawStep = Math.floor(rawTick / ticksPerStep); - const step = loop ? rawStep % steps : Math.min(steps - 1, rawStep); - let phase = basePhase; - if (["PUNCH", "KICK", "WHIP", "BASH", "FIRE", "THROW", "REACH"] - .includes(state)) { - const section = step / Math.max(1, steps - 1); - phase = section < .3 ? "STARTUP" : section < .68 ? "ACTIVE" : "RECOVERY"; - } else if (state === "JUMP" || state === "FALL") { - phase = Math.abs(player.vy) < 150 ? "APEX" : state === "JUMP" - ? "ASCEND" : "DESCEND"; - } - // `frameNow` is the clock, not the cycle. It has to keep climbing with real - // time — the looping states anchor `stateStartedAt` at match start, so a - // cycle-position clock wraps back to the first second forever and every - // "is this timestamp still in the future" test answers yes for the rest of - // the round. That is what pinned a landed fighter in the crouch pose and - // froze the legs. Quantizing to the tick grid keeps the pose on the sim's - // 60 Hz phase without inventing in-between frames. - return { state, phase, step: step + 1, steps, tick: rawTick, - ticksPerStep, progress: step / Math.max(1, steps - 1), - frameNow: stateStartedAt + rawTick * replayTickUs }; -} - -function runnerWorldGeometry(player, t) { - if (player.spiderDummy) return spiderDummyWorldGeometry(player, t); - // Rendering consumes the same fixed 60 Hz phase clock as simulation. The - // display can remain uncapped without inventing in-between combat poses. - const animation = fighterAnimationPhase(player); - const poseNow = animation.frameNow; - const poseCycle = animation.progress * Math.PI * 2; - const speed = Math.min(1, Math.abs(player.vx) / 1500); - const meditating = shellMode === "MENU" && titleTransitionAt === null && - titleAttractMode === "still"; - const idle = player.grounded && !player.ducking && speed < .03; - const breath = idle ? Math.sin(poseCycle + player.pad * .7) * 5 : 0; - const idleSway = idle ? Math.sin(poseCycle + player.pad) * 7 : 0; - const stride = Math.sin(poseCycle + player.pad * Math.PI) * - 32 * speed; - const pogo = isPogo(player); - const headOnly = isHeadOnly(player); - const noLegs = !hasPart(player, "left-leg") && - !hasPart(player, "right-leg") && !headOnly; - const jumpAnticipation = player.jumpLaunchAt > poseNow - ? clamp(1 - (player.jumpLaunchAt - poseNow) / jumpAnticipationUs, 0, 1) : 0; - const landingRecovery = player.landPoseUntil > poseNow - ? clamp((player.landPoseUntil - poseNow) / 110000, 0, 1) : 0; - const crouchPose = clamp(Math.max(player.crouchBlend || 0, - Math.sin(jumpAnticipation * Math.PI) * .72, - landingRecovery * .45, noLegs ? .45 : 0), 0, 1); - const height = lerp(180, 108, crouchPose); - const formDrop = pogo ? 54 : noLegs ? 31 : 0; - const lean = player.facing * (idle ? 5 : 3 + speed * 10); - const x = player.x; - const feet = player.y; - const z = player.z; - // With neither leg present the pelvis itself becomes the ground contact. - // Keeping its capsule tangent to the floor prevents a floating torso pose. - const hipY = noLegs ? feet - 5 - : feet - lerp(58, 40, crouchPose) + formDrop; - const neckX = x + lean; - const neckY = feet - height + 54 - breath + formDrop; - const attackPulse = meleePulse(player, poseNow); - const head = headOnly - ? { x, y: feet - 22, z, radius: 22 } - : { x: neckX + lean * .2, - y: feet - height + 22 - breath * 1.6 + formDrop, z, radius: 22 }; - const segments = []; - const actionArm = player.facing > 0 ? "right-arm" : "left-arm"; - const rearArm = actionArm === "right-arm" ? "left-arm" : "right-arm"; - const actionLeg = player.facing > 0 ? "right-leg" : "left-leg"; - const rearLeg = actionLeg === "right-leg" ? "left-leg" : "right-leg"; - const partForRole = (role, startX) => { - if (["neck", "torso", "shoulders"].includes(role)) return "torso"; - if (role.startsWith("attack-") || role.startsWith("item-")) - return role.includes("arm") || role.includes("forearm") - ? actionArm : actionLeg; - if (role.startsWith("rest-")) return rearArm; - if (role.startsWith("lead-")) return actionLeg; - if (role.startsWith("rear-")) - return role.includes("arm") || role.includes("forearm") - ? rearArm : rearLeg; - if (role.startsWith("grab-")) - return startX <= neckX ? "left-arm" : "right-arm"; - if (role.startsWith("left-")) - return role.includes("arm") || role.includes("forearm") - ? "left-arm" : "left-leg"; - if (role.startsWith("right-")) - return role.includes("arm") || role.includes("forearm") - ? "right-arm" : "right-leg"; - return "torso"; - }; - const segment = (x1, y1, x2, y2, width, role = "body") => { - const part = partForRole(role, x1); - if (!hasPart(player, part)) return; - segments.push({ x1, y1, z1: z, x2, y2, z2: z, width, role, part }); - }; - const twoBone = (startX, startY, targetX, targetY, length, bend) => { - let dx = targetX - startX; - let dy = targetY - startY; - let distance = Math.hypot(dx, dy) || 1; - const maximum = length * 1.94; - if (distance > maximum) { - targetX = startX + dx / distance * maximum; - targetY = startY + dy / distance * maximum; - dx = targetX - startX; - dy = targetY - startY; - distance = maximum; - } - const middleX = (startX + targetX) / 2; - const middleY = (startY + targetY) / 2; - const height = Math.sqrt(Math.max(0, length * length - - distance * distance / 4)); - return { jointX: middleX - dy / distance * height * bend, - jointY: middleY + dx / distance * height * bend, - targetX, targetY }; - }; - segment(head.x, head.y + head.radius * .78, neckX, neckY, 10, "neck"); - segment(neckX, neckY, x, hipY, 10, "torso"); - const shoulderY = neckY + 11; - const shoulderSpread = 12; - const leftShoulderX = neckX - shoulderSpread; - const rightShoulderX = neckX + shoulderSpread; - segment(leftShoulderX, shoulderY, rightShoulderX, shoulderY, 10, - "shoulders"); - if (player.attackKind === "KICK" && attackPulse > 0) { - const target = meleeTarget(player, poseNow); - const leg = twoBone(x, hipY, target.x, target.y, - meleeSpecs.KICK.span, -player.facing); - segment(x, hipY, leg.jointX, leg.jointY, 12, "attack-thigh"); - segment(leg.jointX, leg.jointY, leg.targetX, leg.targetY, 12, - "attack-shin"); - segment(x, hipY, x - player.facing * 28, feet - 32, 10, "rear-thigh"); - segment(x - player.facing * 28, feet - 32, x - player.facing * 8, feet, - 10, "rear-shin"); - } else if (meditating) { - segment(x, hipY, x - 48, feet - 12, 11, "left-thigh"); - segment(x - 48, feet - 12, x + 8, feet, 11, "left-shin"); - segment(x, hipY, x + 48, feet - 12, 11, "right-thigh"); - segment(x + 48, feet - 12, x - 8, feet, 11, "right-shin"); - } else if (crouchPose > .08) { - segment(x, hipY, x - 36, feet - 22, 10, "left-thigh"); - segment(x - 36, feet - 22, x - 4, feet, 10, "left-shin"); - segment(x, hipY, x + 36, feet - 22, 10, "right-thigh"); - segment(x + 36, feet - 22, x + 58, feet, 10, "right-shin"); - } else if (player.grounded) { - // The facing-side foot is visibly planted forward even at rest. - const leadKnee = x + player.facing * 18 + stride * .38; - const leadFoot = x + player.facing * 42 + stride; - const rearKnee = x - player.facing * 16 - stride * .38; - const rearFoot = x - player.facing * 28 - stride; - segment(x, hipY, leadKnee, feet - 30, 10, "lead-thigh"); - segment(leadKnee, feet - 30, leadFoot, feet, 10, "lead-shin"); - segment(x, hipY, rearKnee, feet - 30, 10, "rear-thigh"); - segment(rearKnee, feet - 30, rearFoot, feet, 10, "rear-shin"); - } else { - segment(x, hipY, x - 32, feet - 32, 10, "left-thigh"); - segment(x - 32, feet - 32, x - 7, feet - 11, 10, "left-shin"); - segment(x, hipY, x + 32, feet - 43, 10, "right-thigh"); - segment(x + 32, feet - 43, x + 50, feet - 22, 10, "right-shin"); - } - const arm = idle ? idleSway : player.grounded ? -stride * .7 : 12; - const elbowY = feet - lerp(94, 76, crouchPose) - breath; - const handY = feet - lerp(65, 50, crouchPose) - breath * .5; - const actionNow = poseNow; - const armAttack = attackPulse > 0 && player.attackKind && - player.attackKind !== "KICK"; - if (player.grabHeld) { - const held = player.heldBall >= 0 ? balls[player.heldBall] : null; - const clutchX = held?.x ?? x + player.facing * 126; - const clutchY = held?.y ?? feet - 92; - const spread = held ? Math.max(12, held.radius * .34) : 20; - const hands = [ - { shoulderX: leftShoulderX, x: clutchX - player.facing * 5, - y: clutchY - spread }, - { shoulderX: rightShoulderX, x: clutchX - player.facing * 5, - y: clutchY + spread }, - ]; - for (const hand of hands) { - const pose = twoBone(hand.shoulderX, shoulderY, - hand.x, hand.y, 58, player.facing); - segment(hand.shoulderX, shoulderY, pose.jointX, pose.jointY, 12, - "grab-upper-arm"); - segment(pose.jointX, pose.jointY, pose.targetX, pose.targetY, 12, - "grab-forearm"); - } - } else if (armAttack) { - // The striking arm outranks the carry pose: a whip or bash has to publish - // attack capsules or an armed fighter could never land a hand strike. - const target = meleeTarget(player, poseNow); - const actionShoulderX = player.facing > 0 ? rightShoulderX : leftShoulderX; - const restShoulderX = player.facing > 0 ? leftShoulderX : rightShoulderX; - const armPose = twoBone(actionShoulderX, shoulderY, target.x, target.y, - (meleeSpecs[player.attackKind] || meleeSpecs.PUNCH).span, player.facing); - segment(actionShoulderX, shoulderY, armPose.jointX, armPose.jointY, 12, - "attack-upper-arm"); - segment(armPose.jointX, armPose.jointY, - armPose.targetX, armPose.targetY, 12, "attack-forearm"); - segment(restShoulderX, shoulderY, x - player.facing * 32, elbowY, 10, - "rest-upper-arm"); - segment(x - player.facing * 32, elbowY, - x - player.facing * 36, handY, 10, "rest-forearm"); - } else if ((player.itemAction && actionNow < player.itemActionUntil) || - (player.gunAmmo > 0 && player.itemAction !== "THROW")) { - const target = itemHandTarget(player, actionNow); - const actionShoulderX = player.facing > 0 ? rightShoulderX : leftShoulderX; - const restShoulderX = player.facing > 0 ? leftShoulderX : rightShoulderX; - const armPose = twoBone(actionShoulderX, shoulderY, - target.x, target.y, 58, player.facing); - segment(actionShoulderX, shoulderY, armPose.jointX, armPose.jointY, 12, - "item-upper-arm"); - segment(armPose.jointX, armPose.jointY, - armPose.targetX, armPose.targetY, 12, "item-forearm"); - segment(restShoulderX, shoulderY, x - player.facing * 32, elbowY, 10, - "rest-upper-arm"); - segment(x - player.facing * 32, elbowY, - x - player.facing * 36, handY, 10, "rest-forearm"); - } else { - segment(leftShoulderX, shoulderY, x - 30 + arm * .45, elbowY, 10, - "left-upper-arm"); - segment(x - 30 + arm * .45, elbowY, x - 36 + arm * .6, handY, 10, - "left-forearm"); - segment(rightShoulderX, shoulderY, x + 30 - arm * .45, elbowY, 10, - "right-upper-arm"); - segment(x + 30 - arm * .45, elbowY, x + 36 - arm * .6, handY, 10, - "right-forearm"); - } - return { head, segments }; -} - -// The first spider dummy deliberately shares the inert dummy controller. Its -// threat is physical scale: four independently destructible leg-pairs spread -// across a daddy-long-legs silhouette around one large central body. -function spiderDummyWorldGeometry(player, t) { - const x = player.x; - const feet = player.y; - const z = player.z; - const bodyY = feet - 176; - const sway = Math.sin(t * 1.7 + player.pad) * 5; - const head = { x, y: bodyY + sway, z, radius: 48 }; - const segments = []; - const add = (x1, y1, x2, y2, width, role, part) => { - if (!hasPart(player, part)) return; - segments.push({ x1, y1, z1: z, x2, y2, z2: z, width, role, part }); - }; - add(x - 42, bodyY, x + 42, bodyY, 38, "torso", "torso"); - add(x, bodyY - 30, x, bodyY + 31, 34, "spider-body", "torso"); - const legs = [ - [-1, -1, "left-arm"], [-1, -.55, "left-arm"], - [-1, .25, "left-leg"], [-1, .72, "left-leg"], - [1, -1, "right-arm"], [1, -.55, "right-arm"], - [1, .25, "right-leg"], [1, .72, "right-leg"], - ]; - for (let index = 0; index < legs.length; index++) { - const [side, lane, part] = legs[index]; - const rootX = x + side * 31; - const rootY = bodyY + lane * 31 + sway; - const kneeX = x + side * (150 + Math.abs(lane) * 45); - const kneeY = bodyY - 68 + Math.abs(lane) * 44; - const ankleX = x + side * (270 + Math.abs(lane) * 72); - const ankleY = feet - 42 - (index % 2) * 18; - const footX = ankleX + side * 54; - add(rootX, rootY, kneeX, kneeY, 13, `spider-leg-${index + 1}-upper`, part); - add(kneeX, kneeY, ankleX, ankleY, 11, `spider-leg-${index + 1}-middle`, part); - add(ankleX, ankleY, footX, feet, 9, `spider-leg-${index + 1}-lower`, part); - } - return { head, segments }; -} - -function runnerGeometry(player, t) { - return projectRunnerWorldGeometry(runnerWorldGeometry(player, t)); -} - -function projectRunnerWorldGeometry(world) { - const headPoint = projectPoint(world.head.x, world.head.y, world.head.z); - return { - head: { x: headPoint.x, y: headPoint.y, - radius: Math.max(1.5, world.head.radius * cameraScale()) }, - segments: world.segments.map((segment) => { - const a = projectPoint(segment.x1, segment.y1, segment.z1); - const b = projectPoint(segment.x2, segment.y2, segment.z2); - return { x1: a.x, y1: a.y, x2: b.x, y2: b.y, - width: Math.max(1.5, segment.width * cameraScale()), - role: segment.role, part: segment.part }; - }), - }; -} - -function runnerBounds(player, t) { - const geometry = runnerWorldGeometry(player, t); - let left = geometry.head.x - geometry.head.radius; - let right = geometry.head.x + geometry.head.radius; - let top = geometry.head.y - geometry.head.radius; - let bottom = geometry.head.y + geometry.head.radius; - for (const segment of geometry.segments) { - const radius = segment.width / 2; - left = Math.min(left, segment.x1 - radius, segment.x2 - radius); - right = Math.max(right, segment.x1 + radius, segment.x2 + radius); - top = Math.min(top, segment.y1 - radius, segment.y2 - radius); - bottom = Math.max(bottom, segment.y1 + radius, segment.y2 + radius); - } - return { left, right, top, bottom }; -} - -function runnerScreenBounds(player, t) { - const world = player.replayGeometry || player.frozenGeometry || - runnerWorldGeometry(player, t); - const geometry = projectRunnerWorldGeometry(world); - let left = geometry.head.x - geometry.head.radius; - let right = geometry.head.x + geometry.head.radius; - let top = geometry.head.y - geometry.head.radius; - let bottom = geometry.head.y + geometry.head.radius; - for (const segment of geometry.segments) { - const radius = segment.width / 2; - left = Math.min(left, segment.x1 - radius, segment.x2 - radius); - right = Math.max(right, segment.x1 + radius, segment.x2 + radius); - top = Math.min(top, segment.y1 - radius, segment.y2 - radius); - bottom = Math.max(bottom, segment.y1 + radius, segment.y2 + radius); - } - return { left, right, top, bottom }; -} - -function fighterContainmentRequiredWidth(t) { - let left = Infinity; - let right = -Infinity; - let top = Infinity; - let bottom = -Infinity; - const include = (x, y, radius) => { - left = Math.min(left, x - radius); - right = Math.max(right, x + radius); - top = Math.min(top, y - radius); - bottom = Math.max(bottom, y + radius); - }; - for (const player of players) { - const world = player.replayGeometry || player.frozenGeometry || - runnerWorldGeometry(player, t); - include(world.head.x, world.head.y, world.head.radius); - for (const segment of world.segments) { - const radius = segment.width / 2; - include(segment.x1, segment.y1, radius); - include(segment.x2, segment.y2, radius); - } - } - return Math.max(rectPackWidth({ left, right, top, bottom }), - frameFloorWidth()); -} - -// Final render invariant: both complete animated fighter geometries must fit -// inside the action-safe viewport. Camera modes may orbit or focus, but this -// aspect-aware correction recenters their shared frame and moves the dolly -// back far enough for landscape, portrait, live, and replay projection alike. -function containFighters(t) { - const gameplayContainment = !roundResult && - runtime().monotonicUs - roundStartedAt >= introDurationUs; - if (gameplayContainment) { - cameraContainFloor = Math.max(cameraContainFloor, - fighterContainmentRequiredWidth(t) * 1.11); - return; - } - const worlds = players.map((player) => player.replayGeometry || - player.frozenGeometry || runnerWorldGeometry(player, t)); - if (!worlds.length) return; - const points = []; - for (const world of worlds) { - const { head } = world; - points.push( - { x: head.x - head.radius, y: head.y, z: head.z }, - { x: head.x + head.radius, y: head.y, z: head.z }, - { x: head.x, y: head.y - head.radius, z: head.z }, - { x: head.x, y: head.y + head.radius, z: head.z }, - ); - for (const segment of world.segments) { - const radius = segment.width / 2; - for (const endpoint of [ - { x: segment.x1, y: segment.y1, z: segment.z1 }, - { x: segment.x2, y: segment.y2, z: segment.z2 }, - ]) { - points.push( - { x: endpoint.x - radius, y: endpoint.y, z: endpoint.z }, - { x: endpoint.x + radius, y: endpoint.y, z: endpoint.z }, - { x: endpoint.x, y: endpoint.y - radius, z: endpoint.z }, - { x: endpoint.x, y: endpoint.y + radius, z: endpoint.z }, - ); - } - } - } - const minX = Math.min(...points.map((point) => point.x)); - const maxX = Math.max(...points.map((point) => point.x)); - const minY = Math.min(...points.map((point) => point.y)); - const maxY = Math.max(...points.map((point) => point.y)); - // Keep the smoothly tracked root target. Animated limb extrema are allowed - // to affect containment scale, but never camera aim. - const target = cameraDoll.target; - - const safe = actionSafeRect(); - const safeWidth = Math.max(1, safe.right - safe.left); - const safeHeight = Math.max(1, safe.bottom - safe.top); - const requiredWidth = Math.max( - rectPackWidth({ left: minX, right: maxX, top: minY, bottom: maxY }), - frameFloorWidth()); - if (requiredWidth > cameraDoll.width) { - const amount = requiredWidth / Math.max(1, cameraDoll.width) * 1.04; - for (const axis of ["x", "y", "z"]) - cameraDoll.position[axis] = target[axis] + - (cameraDoll.position[axis] - target[axis]) * amount; - cameraDoll.width *= amount; - } - - // Perspective and z-depth can make the world-space estimate asymmetric. - // Iteratively measure the actual projection and expand along the existing - // camera ray until every point satisfies the screen-space safe rectangle. - const centerX = (safe.left + safe.right) / 2; - const centerY = (safe.top + safe.bottom) / 2; - for (let pass = 0; pass < 4; pass++) { - cameraDoll.dirty = true; - cameraDoll.prepare(); - const projected = points.map((point) => cameraDoll.project(point)); - const left = Math.min(...projected.map((point) => point.x)); - const right = Math.max(...projected.map((point) => point.x)); - const top = Math.min(...projected.map((point) => point.y)); - const bottom = Math.max(...projected.map((point) => point.y)); - const factor = Math.max(1, - (centerX - left) / (safeWidth / 2), - (right - centerX) / (safeWidth / 2), - (centerY - top) / (safeHeight / 2), - (bottom - centerY) / (safeHeight / 2)); - if (factor <= 1.001) break; - const amount = factor * 1.025; - for (const axis of ["x", "y", "z"]) - cameraDoll.position[axis] = target[axis] + - (cameraDoll.position[axis] - target[axis]) * amount; - cameraDoll.width *= amount; - } - cameraDoll.dirty = true; -} - -function resolveRunnerBounds(player, t) { - // Walls use a stable fighting-game pushbox. Animated hands and feet remain - // the actual hit geometry, but cannot shove the root back and forth at an - // arena edge as a pose changes. - const halfWidth = isHeadOnly(player) ? 24 : isPogo(player) ? 38 - : player.ducking ? 76 : 62; - const leftWall = worldLeft + wallThickness; - const rightWall = worldRight - wallThickness; - if (player.x - halfWidth < leftWall) { - player.x = leftWall + halfWidth; - player.vx = Math.max(0, player.vx); - player.knockVx = Math.max(0, player.knockVx); - player.dashUntil = 0; - player.dashVx = 0; - } - if (player.x + halfWidth > rightWall) { - player.x = rightWall - halfWidth; - player.vx = Math.min(0, player.vx); - player.knockVx = Math.min(0, player.knockVx); - player.dashUntil = 0; - player.dashVx = 0; - } - const ceiling = ceilingY + wallThickness; - const standingTop = runnerBounds(player, t).top; - if (standingTop < ceiling) { - player.y += ceiling - standingTop; - if (player.vy < 0) player.vy = 0; - } -} - -function pointSegmentDistance(px, py, pz, segment) { - const dx = segment.x2 - segment.x1; - const dy = segment.y2 - segment.y1; - const dz = segment.z2 - segment.z1; - const lengthSquared = dx * dx + dy * dy + dz * dz; - const amount = lengthSquared > 0 - ? Math.max(0, Math.min(1, - ((px - segment.x1) * dx + (py - segment.y1) * dy + - (pz - segment.z1) * dz) / lengthSquared)) - : 0; - return Math.hypot(px - (segment.x1 + dx * amount), - py - (segment.y1 + dy * amount), pz - (segment.z1 + dz * amount)); -} - -// Closest points between two finite 3D line segments. Fighter limbs use the -// rendered capsule width around these lines, so collision and silhouette share -// the same geometry instead of maintaining a hidden rectangular hitbox. -function segmentSegmentClosest(first, second) { - const d1x = first.x2 - first.x1; - const d1y = first.y2 - first.y1; - const d1z = first.z2 - first.z1; - const d2x = second.x2 - second.x1; - const d2y = second.y2 - second.y1; - const d2z = second.z2 - second.z1; - const rx = first.x1 - second.x1; - const ry = first.y1 - second.y1; - const rz = first.z1 - second.z1; - const a = d1x * d1x + d1y * d1y + d1z * d1z; - const e = d2x * d2x + d2y * d2y + d2z * d2z; - const b = d1x * d2x + d1y * d2y + d1z * d2z; - const c = d1x * rx + d1y * ry + d1z * rz; - const f = d2x * rx + d2y * ry + d2z * rz; - const epsilon = .000000001; - let firstAmount = 0; - let secondAmount = 0; - if (a <= epsilon && e <= epsilon) { - firstAmount = 0; - secondAmount = 0; - } else if (a <= epsilon) { - secondAmount = clamp(f / e, 0, 1); - } else if (e <= epsilon) { - firstAmount = clamp(-c / a, 0, 1); - } else { - const denominator = a * e - b * b; - firstAmount = Math.abs(denominator) > epsilon - ? clamp((b * f - c * e) / denominator, 0, 1) : 0; - const secondNumerator = b * firstAmount + f; - if (secondNumerator < 0) { - secondAmount = 0; - firstAmount = clamp(-c / a, 0, 1); - } else if (secondNumerator > e) { - secondAmount = 1; - firstAmount = clamp((b - c) / a, 0, 1); - } else secondAmount = secondNumerator / e; - } - const firstPoint = { - x: first.x1 + d1x * firstAmount, - y: first.y1 + d1y * firstAmount, - z: first.z1 + d1z * firstAmount, - }; - const secondPoint = { - x: second.x1 + d2x * secondAmount, - y: second.y1 + d2y * secondAmount, - z: second.z1 + d2z * secondAmount, - }; - return { firstPoint, secondPoint, - distance: Math.hypot(firstPoint.x - secondPoint.x, - firstPoint.y - secondPoint.y, firstPoint.z - secondPoint.z) }; -} - -function attackCapsuleContact(attackingLimbs, target, t) { - const targetGeometry = runnerWorldGeometry(target, t); - if (!attackingLimbs.length) return null; - const head = targetGeometry.head; - const headCapsule = { x1: head.x, y1: head.y, z1: head.z, - x2: head.x, y2: head.y, z2: head.z, width: head.radius * 2 }; - let headContact = null; - let bodyContact = null; - for (const attackingLimb of attackingLimbs) { - const headClosest = segmentSegmentClosest(attackingLimb, headCapsule); - const headSeparation = headClosest.distance - - (attackingLimb.width + headCapsule.width) / 2; - if (!headContact || headSeparation < headContact.separation) - headContact = { closest: headClosest, separation: headSeparation }; - for (let index = 0; index < targetGeometry.segments.length; index++) { - const targetLimb = targetGeometry.segments[index]; - const closest = segmentSegmentClosest(attackingLimb, targetLimb); - const separation = closest.distance - - (attackingLimb.width + targetLimb.width) / 2; - if (!bodyContact || separation < bodyContact.separation) - bodyContact = { closest, separation, segmentIndex: index }; - } - } - const headshot = headContact.separation <= 3; - const contact = headshot ? headContact : bodyContact; - if (!contact) return null; - return { - x: (contact.closest.firstPoint.x + contact.closest.secondPoint.x) / 2, - y: (contact.closest.firstPoint.y + contact.closest.secondPoint.y) / 2, - z: (contact.closest.firstPoint.z + contact.closest.secondPoint.z) / 2, - separation: contact.separation, - segmentIndex: headshot ? -1 : contact.segmentIndex, - headshot, - }; -} - -function meleeLimbContact(attacker, target, t) { - const attackingGeometry = runnerWorldGeometry(attacker, t); - const attackingLimbs = attackingGeometry.segments.filter((segment) => - segment.role?.startsWith("attack-")); - return attackCapsuleContact(attackingLimbs, target, t); -} - -function runnerDistanceToPoint(player, t, px, py, pz = 0) { - const contact = runnerContactToPoint(player, t, px, py, pz); - return Math.min(contact.headDistance, contact.bodyDistance); -} - -function runnerContactToPoint(player, t, px, py, pz = 0) { - const geometry = runnerWorldGeometry(player, t); - const headDistance = Math.max(0, - Math.hypot(px - geometry.head.x, py - geometry.head.y, - pz - geometry.head.z) - geometry.head.radius); - let bodyDistance = Infinity; - let segmentIndex = -1; - for (let index = 0; index < geometry.segments.length; index++) { - const segment = geometry.segments[index]; - const distance = Math.max(0, - pointSegmentDistance(px, py, pz, segment) - segment.width / 2); - if (distance < bodyDistance) { - bodyDistance = distance; - segmentIndex = index; - } - } - return { headDistance, bodyDistance, segmentIndex }; -} - -function detachPart(player, part, geometry, sourceX, now) { - if (!hasPart(player, part)) return; - const direction = Math.sign(player.x - sourceX) || player.facing || 1; - for (const segment of geometry.segments.filter((item) => item.part === part)) { - detachedParts.push({ ...segment, color: player.color.slice(), - vx: direction * (420 + detachedParts.length % 3 * 90), - vy: -520 - detachedParts.length % 2 * 120, - spin: direction * (3.5 + detachedParts.length % 4), - life: 2.6, part, owner: player.pad }); - } - player.removedParts.push(part); - player.partDamage[part] = Math.max(player.partDamage[part] || 0, - part === "torso" ? 3 : 2); - emitSignal("partremoved", player.pad, - [...limbParts, "torso"].indexOf(part), 1); -} - -function damagePart(target, segmentIndex, sourceX, sourcePad, now) { - const geometry = runnerWorldGeometry(target, (now - startedAt) / 1000000); - const segment = geometry.segments[segmentIndex]; - const part = segment?.part; - if (!part || !hasPart(target, part)) return; - const durability = part === "torso" ? 3 : 2; - target.partDamage[part] = (target.partDamage[part] || 0) + 1; - emitSignal("partdamage", target.pad, - [...limbParts, "torso"].indexOf(part), target.partDamage[part] / durability); - if (target.partDamage[part] < durability) return; - if (part === "torso") { - // Removing the body's attachment point releases every surviving limb; - // the circular head remains as the final playable form. - for (const limb of limbParts) - detachPart(target, limb, geometry, sourceX, now); - } - detachPart(target, part, geometry, sourceX, now); - target.attackKind = ""; - target.attackUntil = 0; - target.attackHit = false; - target.lastButton = part.toUpperCase() + " LOST"; - target.lastButtonAt = now; - playDrum("clap", 1.2, panPlayer(target)); -} - -function damageAllLimbs(target, sourceX, sourcePad, now) { - for (const part of limbParts) { - if (!hasPart(target, part)) continue; - const geometry = runnerWorldGeometry(target, (now - startedAt) / 1000000); - const index = geometry.segments.findIndex((segment) => segment.part === part); - if (index >= 0) damagePart(target, index, sourceX, sourcePad, now); - } -} - -function applyBodyHit(target, segmentIndex, sourceX, sourcePad, now, - force = 1100, lift = 150, damageParts = true) { - const direction = Math.sign(target.x - sourceX) || - (sourcePad === target.pad ? -target.facing : target.facing) || 1; - releaseCarriedBall(target, now); - target.attackKind = ""; - target.attackUntil = 0; - target.attackHit = false; - target.dashUntil = 0; - target.dashVx = 0; - target.hit = Math.max(target.hit, .52); - target.hitSegment = segmentIndex; - target.hitSegmentUntil = Math.max(target.hitSegmentUntil, now + 190000); - // Balls transfer recoil but are sporting hazards, not damaging attacks. - if (damageParts) - damagePart(target, segmentIndex, sourceX, sourcePad, now); - target.hitStunUntil = Math.max(target.hitStunUntil, now + 145000); - target.knockVx += direction * force; - target.vx = target.knockVx + target.windVx; - target.vy = Math.min(target.vy, -lift); - target.grounded = false; - target.stance = "STUN"; - target.lastButton = "BODY HIT"; - target.lastButtonAt = now; - emitSignal("bodyhit", sourcePad, target.pad, segmentIndex); -} - -function updateDetachedParts(dt) { - for (const fragment of detachedParts) { - fragment.vy += 1900 * dt; - const dx = fragment.vx * dt; - const dy = fragment.vy * dt; - fragment.x1 += dx; - fragment.y1 += dy; - fragment.x2 += dx; - fragment.y2 += dy; - const centerX = (fragment.x1 + fragment.x2) / 2; - const centerY = (fragment.y1 + fragment.y2) / 2; - const angle = fragment.spin * dt; - const cosine = Math.cos(angle); - const sine = Math.sin(angle); - for (const endpoint of [1, 2]) { - const xKey = "x" + endpoint; - const yKey = "y" + endpoint; - const localX = fragment[xKey] - centerX; - const localY = fragment[yKey] - centerY; - fragment[xKey] = centerX + localX * cosine - localY * sine; - fragment[yKey] = centerY + localX * sine + localY * cosine; - } - const bottom = Math.max(fragment.y1, fragment.y2) + fragment.width / 2; - if (bottom > floorY) { - const correction = floorY - bottom; - fragment.y1 += correction; - fragment.y2 += correction; - fragment.vy = -Math.abs(fragment.vy) * .42; - fragment.vx *= .82; - fragment.spin *= .78; - } - fragment.life -= dt; - } - for (let index = detachedParts.length - 1; index >= 0; index--) - if (detachedParts[index].life <= 0) detachedParts.splice(index, 1); -} - -function runnerBodyDistanceToPoint(geometry, px, py, pz = 0) { - let distance = Infinity; - for (const segment of geometry.segments) - distance = Math.min(distance, - Math.max(0, pointSegmentDistance(px, py, pz, segment) - segment.width / 2)); - return distance; -} - -// Comic Relief's packaged horizontal metrics, normalized from its 2048-unit -// em. Colored text is drawn glyph-by-glyph on every host, so using the font's -// real advances here keeps the web canvas and Xbox DirectWrite runs identical. -const comicAdvanceEm = { - a:.512, b:.593, c:.514, d:.587, e:.548, f:.508, g:.531, h:.578, - i:.28, j:.403, k:.54, l:.274, m:.777, n:.523, o:.526, p:.535, - q:.52, r:.48, s:.487, t:.471, u:.52, v:.486, w:.684, x:.59, - y:.521, z:.538, "0":.61, "1":.45, "2":.61, "3":.61, "4":.61, - "5":.61, "6":.61, "7":.61, "8":.61, "9":.61, "@":.931, - "!":.238, "?":.524, ".":.249, ",":.277, ":":.299, ";":.299, - "<":.381, ">":.381, "^":.581, " ":.299, "/":.512, "-":.417, - "+":.48, "∞":.837, -}; - -function comicGlyphAdvance(character, size) { - return size * (comicAdvanceEm[String(character).toLowerCase()] ?? .58); -} - -function handleWidth(handle, size) { - return [...handle].reduce((width, character) => - width + comicGlyphAdvance(character, size), 0); -} - -function typeWrite(text, x, y, size, ...color) { - const visibleText = String(text).toLowerCase(); - if (typeof comicWrite === "function") comicWrite(visibleText, x, y, size, ...color); - else if (typeof ywftWrite === "function") - ywftWrite(visibleText, x, y, size, ...color); - else systemWrite(visibleText, x, y, size, ...color); -} - -function contrastShadow(color) { - const luminance = color[0] * .2126 + color[1] * .7152 + color[2] * .0722; - return luminance > 142 ? [8, 12, 24] : [232, 238, 248]; -} - -// Per-glyph contrast flips a single mid-tone letter to the opposite shadow -// while its neighbors keep theirs, which reads as a mistake rather than as -// depth. A run of type picks its direction once, from the sky behind it, and -// each shadow keeps a trace of its own glyph's hue. -function runShadow(color) { - return mixColor(color, visualTheme.light > .5 ? [10, 12, 26] : [238, 242, 252], - .76); -} - -// Handle colors are authored per name and can be shorter than the name they -// dress. Cycling keeps every glyph on the same palette instead of dropping -// the tail onto a different color source. -function glyphColor(colors, index, fallback) { - return colors?.length ? colors[index % colors.length] : fallback; -} - -function controlLocale() { - const caps = typeof capabilities === "function" ? capabilities() : {}; - const keyboard = caps.inputFamily === "keyboard"; - const touch = caps.inputFamily === "touch"; - if (touch) return { - title: "start", select: "", replayPaused: "paused", - replayPlaying: "", replay: "", - combat: "A KICK B PUNCH X SHIELD Y USE ITEM", - }; - return keyboard ? { - title: "start", - select: "P1 A/D + SPACE P2 LEFT/RIGHT + K H P2/DUMMY/BOT G BACK", - replayPaused: "PAUSED F PLAY A D SCRUB G EXIT", - replayPlaying: "F PAUSE A D SCRUB G EXIT", - replay: "Q REPLAY", - combat: "SPACE KICK ENTER PUNCH SHIFT SHIELD ALT USE ITEM W JUMP", - } : { - title: "start", - select: "LEFT RIGHT SELECT A READY X P2 / DUMMY / BOT B BACK", - replayPaused: "PAUSED A PLAY LEFT RIGHT SCRUB B EXIT", - replayPlaying: "A PAUSE LEFT RIGHT SCRUB B EXIT", - replay: "Y REPLAY", - combat: "A KICK B PUNCH X SHIELD Y USE ITEM UP JUMP", - }; -} - -// Each entry is the cap to draw, what it does, and the button that lights it, -// so the legend can show a key rather than spell one. -function combatKeys() { - const caps = typeof capabilities === "function" ? capabilities() : {}; - if (caps.inputFamily === "keyboard") return [ - ["SPACE", "KICK", "A"], ["ENTER", "PUNCH", "B"], - ["SHIFT", "SHIELD", "X"], ["ALT", "USE ITEM", "Y"], - ["W", "JUMP", "ArrowUp"]]; - if (caps.inputFamily === "touch") return [ - ["A", "KICK", "A"], ["B", "PUNCH", "B"], - ["X", "SHIELD", "X"], ["Y", "USE ITEM", "Y"]]; - return [ - ["A", "KICK", "A"], ["B", "PUNCH", "B"], ["X", "SHIELD", "X"], - ["Y", "USE ITEM", "Y"], ["STICK_UP", "JUMP", "ArrowUp"]]; -} - -// What the buttons do, named on the way into a round. B changes meaning with -// what the hand is carrying, so the legend reads the fighter rather than -// reciting a fixed map. -function combatLegendKeys(player) { - const swing = itemMelee[heldItem(player)]; - return combatKeys().map(([cap, action, button]) => - [cap, swing && action === "PUNCH" ? swing : action, button]); -} - -function combatLegend(player) { - return combatLegendKeys(player) - .map(([cap, action]) => (cap === "STICK_UP" ? "STICK" : cap) + - " " + action).join(" "); -} - -function selectionControlKeys() { - const family = typeof capabilities === "function" - ? capabilities().inputFamily : "xbox"; - if (family === "touch") return []; - if (family === "keyboard") return [ - [["A", "D"], "SELECT", "ArrowLeft"], ["SPACE", "READY", "A"], - ["SHIFT", "OPPONENT", "X"], ["ENTER", "BACK", "B"]]; - return [[["LEFT", "RIGHT"], "SELECT", "ArrowLeft"], ["A", "READY", "A"], - ["X", "OPPONENT", "X"], ["B", "BACK", "B"]]; -} - -function replayControlKeys(paused) { - const family = typeof capabilities === "function" - ? capabilities().inputFamily : "xbox"; - if (family === "touch") return []; - if (family === "keyboard") return [ - ["F", paused ? "PLAY" : "PAUSE", "A"], - [["A", "D"], "SCRUB", "ArrowLeft"], ["G", "EXIT", "B"]]; - return [["A", paused ? "PLAY" : "PAUSE", "A"], - [["LEFT", "RIGHT"], "SCRUB", "ArrowLeft"], ["B", "EXIT", "B"]]; -} - -function replayOfferKeys() { - const family = typeof capabilities === "function" - ? capabilities().inputFamily : "xbox"; - if (family === "touch") return []; - return family === "keyboard" ? [["Q", "REPLAY", "Y"]] - : [["Y", "REPLAY", "Y"]]; -} - -// A key drawn as a key: a gray cap with a line around it, sunk and brightened -// while it is actually held. -// `fade` is how present the cap is: 1 draws it, 0 dissolves it into the stage -// behind. The legend always passes 1; only the command stream ages its caps -// out, and it needs the whole cap to go — face, edge and letter together. -// A controller button drawn as a controller button: the letter in its Xbox -// color on a dark disc with a colored rim; pressed inverts to a filled disc. -// Directions are the same disc with an arrow glyph in neutral ink. Keyboard -// input keeps its keycaps; every other family gets these. -const padButtonInk = { - A: [96, 200, 80], B: [235, 78, 78], X: [86, 148, 235], Y: [240, 198, 60], -}; -const padGlyph = { UP: "↑", DOWN: "↓", LEFT: "←", RIGHT: "→" }; -const padButtonDiameter = (size) => Math.round(size * .78) * 2; -function drawPadButton(label, x, y, size, pressed, fade = 1) { - const radius = Math.round(size * .78); - if (fade <= .01) return radius * 2; - const ground = mixColor([7, 8, 28], [230, 239, 247], visualTheme.light); - const veil = (color) => fade >= 1 ? color : mixColor(ground, color, fade); - const ink = padButtonInk[label] || mixColor([148, 158, 178], [96, 104, 124], - visualTheme.light); - // Filled like the real thing: the color IS the button. Held brightens it - // and rims it white; the glyph stays dark and dead-center either way. - const face = pressed ? mixColor(ink, [255, 255, 255], .3) : ink; - const cx = x + radius; - const cy = y + Math.round(size * .75); - filledDisc(cx, cy, radius, veil(face)); - if (pressed) filledRing(cx, cy, radius, radius - 3, veil([245, 248, 255])); - const text = (padGlyph[label] || label).toUpperCase(); - const glyphSize = Math.round(size * .82); - if (label === "STICK_UP") { - const arrowInk = veil([12, 14, 26]); - const thickness = Math.max(2, Math.round(radius * .11)); - const tipY = cy - radius * .42; - const wingY = cy - radius * .08; - filledCapsule(cx, cy + radius * .4, cx, tipY, thickness, arrowInk); - filledCapsule(cx, tipY, cx - radius * .32, wingY, thickness, arrowInk); - filledCapsule(cx, tipY, cx + radius * .32, wingY, thickness, arrowInk); - return radius * 2; - } - const glyphX = Math.round(cx - handleWidth(text, glyphSize) / 2); - // DirectWrite's cap shapes carry more visual weight below their nominal - // midpoint. Lift the Xbox face letters together so A/B/X/Y read centered in - // the colored hardware circles rather than sitting on their lower halves. - const glyphY = Math.round(cy - glyphSize * (padButtonInk[label] ? .62 : .5)); - if (padButtonInk[label] || /^[KPSI<>^v]$/.test(text)) - systemWrite(text, glyphX, glyphY, glyphSize, ...veil([12, 14, 26])); - else typeWrite(text, glyphX, glyphY, glyphSize, ...veil([12, 14, 26])); - return radius * 2; -} - -function drawKeycap(label, x, y, size, pressed, fade = 1) { - const padX = Math.round(size * .42); - const height = Math.round(size * 1.5); - const width = handleWidth(label, size) + padX * 2; - if (fade <= .01) return width; - const drop = pressed ? 2 : 0; - const ground = mixColor([7, 8, 28], [230, 239, 247], visualTheme.light); - const veil = (color) => fade >= 1 ? color : mixColor(ground, color, fade); - const face = veil(pressed - ? mixColor([96, 104, 126], [206, 214, 232], visualTheme.light) - : mixColor([44, 50, 66], [176, 184, 202], visualTheme.light)); - const edge = veil(pressed - ? mixColor([210, 220, 240], [40, 46, 62], visualTheme.light) - : mixColor([112, 122, 146], [96, 104, 124], visualTheme.light)); - if (!pressed) - box(x + 2, y + 4, width, height, ...veil(mixColor([6, 8, 18], [92, 99, 112], - visualTheme.light * .7))); - box(x, y + drop, width, height, ...face); - strokeBox(x, y + drop, width, height, 2, edge); - typeWrite(label, x + padX, y + drop + Math.round((height - size) / 2), size, - ...veil(pressed ? [12, 14, 26] : [238, 242, 252])); - return width; -} - -// Lifted clear of the bottom edge, because the signed-in handle and its -// logout button now sit in that corner and the legend used to run straight -// through them on a narrow view. -function drawControlLegend(ink) { - const entries = combatLegendKeys(players[0]); - const size = compactLayout() ? 18 : 24; - const safe = hudSafeRect(); - drawKeycapRun(entries, safe.left + 8, - safe.top + hudTypeSize + 18, size, - inputPads[0]?.down || [], ink); -} - -// Touch is part of the game surface, not a DOM overlay. One drawn d-pad and -// four action discs share the exact centers used by mac-test's canvas hit zones. -function drawTouchControls() { - if (typeof capabilities !== "function" || - capabilities().inputFamily !== "touch" || capabilities().socialPreview) - return; - const held = inputPads[0]?.down || []; - const spread = 64; - const cy = viewHeight - 140; - const dpadX = 130; - const idle = mixColor([58, 66, 86], [170, 180, 196], visualTheme.light); - const live = mixColor([110, 220, 150], [38, 128, 88], visualTheme.light); - const arm = 38; - const thick = 18; - const directions = [[0, -1, "ArrowUp"], [0, 1, "ArrowDown"], - [-1, 0, "ArrowLeft"], [1, 0, "ArrowRight"]]; - for (const [dx, dy, key] of directions) - filledCapsule(dpadX + dx * 8, cy + dy * 8, - dpadX + dx * arm, cy + dy * arm, thick, - held.includes(key) ? live : idle); - filledDisc(dpadX, cy, thick, held.some((key) => key.startsWith("Arrow")) - ? live : idle); - const actionX = viewWidth() - 130; - for (const [dx, dy, key] of [[0, -1, "Y"], [0, 1, "A"], - [-1, 0, "X"], [1, 0, "B"]]) - drawPadButton(key, actionX + dx * spread - 27, - cy + dy * spread - 25, 35, held.includes(key)); -} - -function keycapRunWidth(entries, size) { - const keyboard = keycapFamily(); - const padX = Math.round(size * .42); - const capWidth = (label) => keyboard - ? handleWidth(label, size) + padX * 2 : padButtonDiameter(size); - return entries.reduce((total, [cap, action]) => - total + (Array.isArray(cap) ? cap : [cap]).reduce((width, label, index) => - width + capWidth(label) + (index ? 5 : 0), 0) + 8 + - handleWidth(action, size) + 26, 0) - 26; -} - -// Keyboard reads as keycaps; every other family reads as controller discs. -const keycapFamily = () => (typeof capabilities === "function" - ? String(capabilities().inputFamily || "") : "") === "keyboard"; - -function drawKeycapRun(entries, x, y, size, held, ink) { - const keyboard = keycapFamily(); - let cursor = x; - for (const [cap, action, button] of entries) { - for (const [index, label] of (Array.isArray(cap) ? cap : [cap]).entries()) { - if (index) cursor += 5; - cursor += keyboard - ? drawKeycap(label, cursor, y, size, held.includes(button)) - : drawPadButton(label, cursor, y, size, held.includes(button)); - } - cursor += 8; - typeWrite(action, cursor, y + Math.round(size * .25), size, ...ink); - cursor += handleWidth(action, size) + 26; - } -} - -function drawCenteredKeycapRun(entries, y, size, held, ink) { - if (!entries.length) return; - drawKeycapRun(entries, viewCenterX() - keycapRunWidth(entries, size) / 2, - y, size, held, ink); -} - -function drawHandle(handle, x, y, size, colors, fallback) { - let cursor = x; - const characters = [...String(handle)]; - for (let index = 0; index < characters.length; index++) { - typeWrite(characters[index], cursor, y, size, - ...glyphColor(colors, index, fallback)); - cursor += comicGlyphAdvance(characters[index], size); - } -} - -function drawFace(player, head, color, t, now = runtime().monotonicUs) { - if (head.radius < 5) return; - const r = head.radius; - const direction = player.facing || 1; - const stroke = (x1, y1, x2, y2, width, ink = color) => - filledCapsule(x1, y1, x2, y2, width, ink); - if (player.name === "@FIFI") { - const hair = visualTheme.light > .55 ? [105, 38, 116] : [245, 118, 230]; - const sway = Math.sin(t * 2.2 + player.pad) * r * .08; - const width = Math.max(2, r * .13); - stroke(head.x - r * .72, head.y - r * .48, - head.x - r * .32, head.y - r * .9, width, hair); - stroke(head.x - r * .32, head.y - r * .9, - head.x + r * .35, head.y - r * .88, width, hair); - stroke(head.x + r * .35, head.y - r * .88, - head.x + r * .76, head.y - r * .38, width, hair); - stroke(head.x - r * .76, head.y - r * .38, - head.x - r * .7 + sway, head.y + r * .92, width, hair); - stroke(head.x + r * .76, head.y - r * .38, - head.x + r * .7 + sway, head.y + r * .92, width, hair); - stroke(head.x - r * .48, head.y - r * .46, - head.x - r * .08, head.y - r * .18, width * .72, hair); - stroke(head.x - r * .08, head.y - r * .18, - head.x + r * .42, head.y - r * .5, width * .72, hair); - } - const eyeY = head.y - r * .18; - const eyeGap = r * .34; - const faceX = head.x + direction * r * .08; - const eyeWidth = Math.max(1.4, r * .1); - const lineWidth = Math.max(1.2, r * .1); - const blink = player.alive && !player.blocking && !player.attackKind && - Math.sin(t * .73 + player.pad * 2.1) > .985; - const inertDummy = player.npc && !player.bot; - if (!player.alive || player.hit > .6 || inertDummy) { - for (const offset of [-eyeGap, eyeGap]) { - stroke(faceX + offset - eyeWidth, eyeY - eyeWidth, - faceX + offset + eyeWidth, eyeY + eyeWidth, lineWidth); - stroke(faceX + offset + eyeWidth, eyeY - eyeWidth, - faceX + offset - eyeWidth, eyeY + eyeWidth, lineWidth); - } - } else if (player.blocking || blink) { - stroke(faceX - eyeGap - eyeWidth, eyeY, faceX - eyeGap + eyeWidth, - eyeY, lineWidth); - stroke(faceX + eyeGap - eyeWidth, eyeY, faceX + eyeGap + eyeWidth, - eyeY, lineWidth); - } else { - const pad = inputPads[player.pad] || {}; - const rawX = Number(pad.leftX) || 0; - const rawY = Number(pad.leftY) || 0; - const deadzone = .48; - const digitalX = (pad.down || []).includes("ArrowRight") ? 1 - : (pad.down || []).includes("ArrowLeft") ? -1 : 0; - const digitalY = (pad.down || []).includes("ArrowUp") ? 1 - : (pad.down || []).includes("ArrowDown") ? -1 : 0; - const lookX = digitalX || (Math.abs(rawX) >= deadzone ? rawX : 0) || direction * .2; - const lookY = digitalY || (Math.abs(rawY) >= deadzone ? rawY : 0); - const pupilX = clamp(lookX, -1, 1) * eyeWidth * 1.05; - const pupilY = clamp(-lookY, -1, 1) * eyeWidth * 1.05; - const socketRadius = eyeWidth * 2.35; - const pupilRadius = eyeWidth * .9; - const pupilInk = visualTheme.light > .5 ? [10, 14, 28] : [244, 248, 255]; - for (const offset of [-eyeGap, eyeGap]) { - filledDisc(faceX + offset, eyeY, socketRadius, color); - filledDisc(faceX + offset + pupilX, eyeY + pupilY, - pupilRadius, pupilInk); - } - } - if (player.bot && player.alive && !player.blocking) { - const browY = eyeY - r * .32; - stroke(faceX - eyeGap - eyeWidth * 1.25, browY - r * .08, - faceX - eyeGap + eyeWidth * 1.25, browY + r * .1, - lineWidth * 1.15); - stroke(faceX + eyeGap - eyeWidth * 1.25, browY + r * .1, - faceX + eyeGap + eyeWidth * 1.25, browY - r * .08, - lineWidth * 1.15); - } - const mouthY = head.y + r * .3; - if (player.attackKind && meleePulse(player, now) > 0) { - filledRing(faceX + direction * r * .12, mouthY, - Math.max(1.8, r * .13), Math.max(.4, r * .05), color); - } else if (player.blocking) { - stroke(faceX - r * .26, mouthY, faceX + r * .26, mouthY, lineWidth); - } else if (!player.alive || player.hit > .6) { - stroke(faceX - r * .24, mouthY + r * .08, faceX, - mouthY - r * .06, lineWidth); - stroke(faceX, mouthY - r * .06, faceX + r * .24, - mouthY + r * .08, lineWidth); - } else { - const smile = Math.sin(t * 2.4 + player.pad) * r * .035; - stroke(faceX - r * .23, mouthY - smile, faceX, - mouthY + r * .09, lineWidth); - stroke(faceX, mouthY + r * .09, faceX + r * .23, - mouthY - smile, lineWidth); - } -} - -function drawInventory(player, now) { - const scale = cameraScale(); - const gunColor = [255, 220, 72]; - const grenadeColor = [255, 105, 105]; - const firing = player.itemAction === "FIRE" && now < player.itemActionUntil; - const throwing = player.itemAction === "THROW" && now < player.itemActionUntil; - // A bash keeps the grenade visible in the fist; itemHandTarget already - // parks both weapons on the swinging hand. - const bashing = player.attackKind === "BASH" && itemSwinging(player, now); - if ((player.gunAmmo > 0 || firing) && !throwing) { - const pose = gunPose(player, now); - const hand = projectPoint(pose.hand.x, pose.hand.y, pose.hand.z); - const barrel = projectPoint(pose.muzzle.x, pose.muzzle.y, pose.muzzle.z); - line(hand.x, hand.y, barrel.x, barrel.y, - Math.max(3, 9 * scale), ...gunColor); - line(hand.x, hand.y, hand.x - player.facing * 8 * scale, - hand.y + 20 * scale, Math.max(2, 6 * scale), ...gunColor); - if (firing) { - const normalX = -pose.dy; - const normalY = pose.dx; - const flashA = projectPoint( - pose.muzzle.x + pose.dx * 28 + normalX * 18, - pose.muzzle.y + pose.dy * 28 + normalY * 18, pose.muzzle.z); - const flashB = projectPoint( - pose.muzzle.x + pose.dx * 28 - normalX * 18, - pose.muzzle.y + pose.dy * 28 - normalY * 18, pose.muzzle.z); - line(barrel.x, barrel.y, flashA.x, flashA.y, - Math.max(2, 5 * scale), 255, 248, 190); - line(barrel.x, barrel.y, flashB.x, flashB.y, - Math.max(2, 5 * scale), 255, 248, 190); - } - } - if (throwing || bashing) { - const target = itemHandTarget(player, now); - const hand = projectPoint(target.x, target.y, target.z); - circle(hand.x, hand.y, Math.max(5, 15 * scale), - Math.max(2, 5 * scale), grenadeColor); - } -} - -function drawDigitalHeadBurst(player, headWorld, age) { - const burstAge = Math.max(0, age - .11); - const origin = projectPoint(headWorld.x, headWorld.y, headWorld.z); - if (![origin.x, origin.y, origin.z].every(Number.isFinite) || - Math.abs(origin.x) > 30000 || Math.abs(origin.y) > 30000) return; - const palette = [ - [255, 48, 96], [176, 18, 54], [255, 92, 126], - player.color, [116, 8, 38], - ]; - const directions = [-2.92, -2.61, -2.3, -2.01, -1.72, -1.43, - -1.14, -.83, -.52, -.21, .1, .41, .72, 1.03]; - for (let index = 0; index < directions.length; index++) { - const angle = directions[index] + (player.pad ? .14 : -.14); - const speed = 48 + index * 4; - const fall = 10 * burstAge + 72 * burstAge * burstAge; - for (let speck = 0; speck < 3; speck++) { - const spread = .38 + speck * .29; - const distance = (5 + speed * burstAge) * spread; - const depth = (index % 3 - 1) * (3 + burstAge * 9) * spread; - const point = projectPoint( - headWorld.x + Math.cos(angle) * distance, - headWorld.y + Math.sin(angle) * distance + fall * spread, - headWorld.z + depth); - if (![point.x, point.y, point.z].every(Number.isFinite) || - Math.abs(point.x) > 30000 || Math.abs(point.y) > 30000) continue; - triangleDepth = point.z; - const pixel = clamp((2.6 - speck * .48) * cameraScale(), 1.5, 4); - screenRect(point.x - pixel / 2, point.y - pixel / 2, - pixel, pixel, palette[(index + speck) % palette.length]); - } - } - // The face becomes a small broken core rather than remaining an intact disc. - const core = Math.max(2, headWorld.radius * cameraScale() * - Math.max(.08, 1 - burstAge * 6.2)); - triangleDepth = origin.z; - for (let index = 0; index < 4; index++) { - const side = index % 2 ? 1 : -1; - const row = index < 2 ? -1 : 1; - const size = core * (.62 + index * .05); - screenRect(origin.x + side * core * .32 - size / 2, - origin.y + row * core * .3 - size / 2, size, size, - palette[index % palette.length]); - } -} - -function drawBrokenRunner(player, age) { - const world = player.frozenGeometry || - runnerWorldGeometry(player, (runtime().monotonicUs - startedAt) / 1000000); - const slow = Math.max(0, age - .11) * .32; - const gravity = 420 * slow * slow; - const outline = [8, 12, 24]; - for (let index = 0; index < world.segments.length; index++) { - const segment = world.segments[index]; - const midX = (segment.x1 + segment.x2) / 2; - const midY = (segment.y1 + segment.y2) / 2; - const side = index % 2 ? 1 : -1; - const dx = (side * (72 + index * 9) - player.facing * 34) * slow; - const dy = -(150 + index * 13) * slow + gravity; - const dz = side * (24 + index * 4) * slow; - const angle = side * slow * (1.15 + index * .08); - const cosine = Math.cos(angle); - const sine = Math.sin(angle); - const move = (x, y, z) => { - const localX = x - midX; - const localY = y - midY; - return { x: midX + localX * cosine - localY * sine + dx, - y: midY + localX * sine + localY * cosine + dy, - z: z + dz }; - }; - const first = move(segment.x1, segment.y1, segment.z1); - const second = move(segment.x2, segment.y2, segment.z2); - const a = projectPoint(first.x, first.y, first.z); - const b = projectPoint(second.x, second.y, second.z); - if (![a.x, a.y, b.x, b.y].every(Number.isFinite) || - [a.x, a.y, b.x, b.y].some((value) => Math.abs(value) > 30000)) - continue; - triangleDepth = projectPoint(midX + dx, midY + dy, - (segment.z1 + segment.z2) / 2 + dz).z; - const width = Math.max(2, segment.width * cameraScale()); - filledCapsule(a.x, a.y, b.x, b.y, width + 3, outline); - filledCapsule(a.x, a.y, b.x, b.y, width, player.color); - } - drawDigitalHeadBurst(player, world.head, age); -} - -function deathCinematicAge(now = runtime().monotonicUs) { - return deathCinematic ? Math.max(0, (now - deathCinematic.startedAt) / 1000000) : -1; -} - -function drawDeathFlash() { - const age = deathCinematicAge(); - if (age < 0 || age >= .11) return; - const previousDepth = triangleDepth; - triangleDepth = -1.425; - const flash = age < .055 ? [255, 255, 255] : [255, 226, 116]; - screenRect(0, 0, viewWidth(), viewHeight, flash); - triangleDepth = previousDepth; -} - -function drawRunner(player, t, showLabel = true) { - // A fighter who is down is still on the stage: they read as the broken body - // rather than as nothing. There is no state in which a fighter is skipped. - const headBurstAge = player.headBustedAt - ? Math.max(0, (runtime().monotonicUs - player.headBustedAt) / 1000000) : -1; - if (!player.alive && headBurstAge >= .11) { - drawBrokenRunner(player, headBurstAge); - return; - } - const geometry = player.replayGeometry - ? projectRunnerWorldGeometry(player.replayGeometry) - : player.frozenGeometry - ? projectRunnerWorldGeometry(player.frozenGeometry) - : runnerGeometry(player, t); - // Preserve the fighter's identity color during hit flash. A pure white body - // disappeared against the daylight arena, so impact now changes only its rim. - const color = player.color; - const outline = player.hit > 0 - ? mixColor([255, 232, 92], [28, 34, 52], visualTheme.light) - : [8, 12, 24]; - const displayNow = player.frozenAt || runtime().monotonicUs; - drawFighterSilhouette(geometry, color, outline, player); - const hitNow = runtime().monotonicUs; - if (player.hitSegment >= 0 && hitNow < player.hitSegmentUntil && - Math.floor(hitNow / 45000) % 2 === 0) { - const segment = geometry.segments[player.hitSegment]; - if (segment) { - filledCapsule(segment.x1, segment.y1, segment.x2, segment.y2, - segment.width + Math.max(3, 5 * cameraScale()), [255, 238, 102]); - } - } - if (shellMode !== "MENU" || titleTransitionAt !== null || - titleAttractMode === "action") - drawFace(player, geometry.head, contrastShadow(color), t, displayNow); - drawInventory(player, displayNow); - if (player.blocking) { - const worldShield = shieldGeometry(player); - const shield = projectPoint(worldShield.x, worldShield.y, worldShield.z); - const radius = Math.max(18, worldShield.radius * cameraScale()); - const shieldColor = player.blockFlash > 0 ? [255, 255, 255] : player.color; - const outerWidth = Math.max(4, 11 * cameraScale()); - const innerWidth = Math.max(3, 7 * cameraScale()); - filledRing(shield.x, shield.y, radius, - Math.max(0, radius - outerWidth), shieldColor); - filledRing(shield.x, shield.y, radius * .76, - Math.max(0, radius * .76 - innerWidth), shieldColor); - filledCapsule(shield.x + player.facing * radius * .25, shield.y - radius * .72, - shield.x + player.facing * radius * .72, shield.y + radius * .72, - Math.max(4, 8 * cameraScale()), shieldColor); - } - -} - -// Hitboxes are an inspector, not a garnish. They used to flash on every -// impact for anyone watching, which put green boxes over ordinary play and -// over every recording of it. VIEW (tab) is now the only thing that shows -// them — `impactHitboxesUntil` still times the flash, but only for someone -// who has already asked to see the geometry. -function drawDebugHitboxes(player, t) { - const now = runtime().monotonicUs; - const impactDebug = debugHitboxes && !roundResult && now < impactHitboxesUntil; - if ((!debugHitboxes && !impactDebug) || (!player.alive && !roundResult)) return; - const cinematicAge = deathCinematicAge(now); - if (deathCinematic?.loserPad === player.pad && cinematicAge >= .11) return; - const world = player.replayGeometry || player.frozenGeometry || - runnerWorldGeometry(player, t); - const geometry = projectRunnerWorldGeometry(world); - const bodyColor = [58, 222, 255]; - const headColor = [255, 62, 82]; - const pushColor = [105, 255, 118]; - const attackColor = [255, 86, 220]; - - for (const segment of geometry.segments) - filledCapsule(segment.x1, segment.y1, segment.x2, segment.y2, - Math.max(2, segment.width * .22), bodyColor); - filledRing(geometry.head.x, geometry.head.y, geometry.head.radius, - Math.max(0, geometry.head.radius - Math.max(2, - geometry.head.radius * .12)), headColor); - - const halfWidth = player.ducking ? 76 : 62; - const top = player.y - (player.ducking ? 132 : 174); - const corners = [ - projectPoint(player.x - halfWidth, top, player.z), - projectPoint(player.x + halfWidth, top, player.z), - projectPoint(player.x + halfWidth, player.y, player.z), - projectPoint(player.x - halfWidth, player.y, player.z), - ]; - for (let index = 0; index < corners.length; index++) { - const next = corners[(index + 1) % corners.length]; - filledCapsule(corners[index].x, corners[index].y, - next.x, next.y, 2, pushColor); - } - - const displayNow = player.frozenAt || now; - if (player.attackKind && displayNow < player.attackUntil) { - for (const segment of geometry.segments) { - if (!segment.role?.startsWith("attack-")) continue; - filledCapsule(segment.x1, segment.y1, segment.x2, segment.y2, - segment.width + 5, attackColor); - } - } -} - -function drawPlayerHud(player, x, pad) { - const color = visualTheme.light > .55 - ? player.pad === 0 ? [155, 34, 108] : [105, 78, 0] - : player.color; - let label = "P" + (player.pad + 1) + " " + player.roundWins + "/" + - matchWins + " PTS " + player.score; - typeWrite(label, x, 14, 22, ...color); -} - -function drawFighterData(player, x) { - const profile = fighterProfile(player.name); - const mood = profile.mood ? "M " + profile.mood.slice(0, 24) : "M —"; - const chat = profile.lastChat ? "CHAT " + profile.lastChat.slice(0, 34) : "CHAT —"; - const ink = mixColor([190, 205, 235], [55, 66, 90], visualTheme.light); - typeWrite(mood + " · " + chat, x, 82, 15, ...ink); -} - -function visibleHandle(player) { - return player.name.toLowerCase(); -} - -function playerHandleLayout(player, side) { - const safe = hudSafeRect(); - const touch = typeof capabilities === "function" && - capabilities().inputFamily === "touch"; - const size = hudTypeSize; - const width = handleWidth(visibleHandle(player), size); - const x = side === 0 ? safe.left + 8 : safe.right - 8 - width; - const y = safe.bottom - size - (touch ? 250 : 18); - return { x, y, size, width }; -} - -function drawFloatingHandle(player, x, y, size) { - const handle = visibleHandle(player); - const shadows = player.handleColors?.map(runShadow); - drawHandle(handle, x + 3, y + 4, size, shadows, runShadow(player.color)); - drawHandle(handle, x, y, size, player.handleColors, player.color); -} - -function drawPlayerHandle(player, t, side) { - const { x, y, size } = playerHandleLayout(player, side); - const handle = visibleHandle(player); - const drawGlyphs = (dx, dy, colors, fallback) => { - let cursor = x + dx; - for (let index = 0; index < handle.length; index++) { - const character = handle[index]; - typeWrite(character, cursor, y + dy, size, - ...glyphColor(colors, index, fallback)); - cursor += comicGlyphAdvance(character, size); - } - }; - drawGlyphs(3, 4, player.handleColors?.map(runShadow), runShadow(player.color)); - drawGlyphs(0, 0, player.handleColors, player.color); -} - -function playerStatLines(player) { - const pad = inputPads[player.pad] || - { connected: false, down: [], leftX: 0, leftY: 0 }; - const input = quantizedInput(pad, player.suppressedDirections); - const animation = fighterAnimationPhase(player, - player.frozenAt || runtime().monotonicUs); - return [ - "p" + (player.pad + 1) + " :: " + player.stance + - (player.attackKind ? " + " + player.attackKind : ""), - "in[" + input.horizontal + "," + input.vertical + "] -> " + - "stk[" + pad.leftX.toFixed(2) + "] vx[" + Math.round(player.vx) + "]", - "anim::" + animation.state + " step[" + animation.step + "/" + - animation.steps + "] t[" + animation.tick + "]", - ]; -} - -const playerStatSize = () => compactLayout() ? 16 : 22; -const playerStatPanelHeight = () => { - const size = playerStatSize(); - return 16 + 3 * (size + 7) + 10; -}; - -// The bordered state card owns a fixed stack over each handle. Its rows read -// like a tiny trace program, while the segmented foot shows animation progress -// without asking the numeric step label to carry the whole explanation. -const statStackHeight = () => debugHitboxes ? playerStatPanelHeight() + 8 : 0; - -function drawPlayerStats(player, side, t) { - if (!debugHitboxes) return; - const lines = playerStatLines(player); - const animation = fighterAnimationPhase(player, - player.frozenAt || runtime().monotonicUs); - const size = playerStatSize(); - const padding = 8; - const lineHeight = size + 7; - const safe = hudSafeRect(); - const contentWidth = Math.max(...lines.map((text) => handleWidth(text, size))); - const width = contentWidth + padding * 2; - const height = playerStatPanelHeight(); - const handle = playerHandleLayout(player, side); - const x = side === 0 ? safe.left : safe.right - width; - const y = handle.y - height - 12; - const face = mixColor([10, 14, 30], [226, 235, 242], visualTheme.light); - const edge = mixColor(player.color, [28, 34, 48], visualTheme.light * .45); - box(x + 4, y + 5, width, height, ...runShadow(edge)); - box(x, y, width, height, ...face); - strokeBox(x, y, width, height, 2, edge); - for (let row = 0; row < lines.length; row++) { - const rowY = y + padding + row * lineHeight; - if (row) box(x + 5, rowY - 4, width - 10, 1, ...edge); - writeHudLine(lines[row], x + padding, rowY, size); - } - const count = Math.max(1, Math.min(16, animation.steps)); - const gap = 3; - const trackX = x + padding; - const trackY = y + height - 9; - const segmentWidth = (width - padding * 2 - gap * (count - 1)) / count; - for (let step = 0; step < count; step++) { - const active = step <= Math.min(count - 1, animation.step); - box(trackX + step * (segmentWidth + gap), trackY, - Math.max(2, segmentWidth), 4, ...(active ? player.color : edge)); - } -} - -function drawHudInventory(player, side) { - const items = []; - if (player.gunAmmo > 0) items.push("gun " + player.gunAmmo); - if (player.grenadeAmmo > 0) items.push("grenade " + player.grenadeAmmo); - if (!items.length) return; - const text = items.join(" "); - const handle = playerHandleLayout(player, side); - const size = Math.round(hudTypeSize * .62); - const width = handleWidth(text, size); - const x = side === 0 ? handle.x : handle.x + handle.width - width; - const y = handle.y - statStackHeight() - size - 7; - const shadow = contrastShadow(player.color); - typeWrite(text, x + 2, y + 3, size, ...shadow); - typeWrite(text, x, y, size, ...player.color); -} - -// How present one glyph of the buffer is. `settle` is how far into the idle -// the dissolve has travelled: 0 for as long as the pad is still saying -// something, 1 once the whole stream has left. Doubling it and offsetting by -// each glyph's place in the stream spends the first half on the oldest glyph -// and the last half on the newest, so the buffer leaves in reading order -// rather than dimming as one sheet. -function commandFade(index, count, idle) { - const settle = clamp((idle - commandHoldUs) / commandFadeUs, 0, 1); - return clamp(index / Math.max(1, count - 1) + 1 - settle * 2, 0, 1); -} - -function drawCommandStream(player, side) { - const glyph = { LEFT: "<", RIGHT: ">", UP: "^", DOWN: "v" }; - const buttonFor = { LEFT: "ArrowLeft", RIGHT: "ArrowRight", - UP: "ArrowUp", DOWN: "ArrowDown", "/": "A", "*": "B", ")": "X", - "+": ["Y", "LeftShoulder", "RightShoulder"] }; - const now = runtime().monotonicUs; - const idle = now - (player.commandStream.at(-1)?.at || now); - const count = player.commandStream.length; - const held = inputPads[player.pad]?.down || []; - const entries = player.commandStream.map((entry, index) => ({ ...entry, - text: glyph[entry.label] || entry.label, - fade: commandFade(index, count, idle), - held: Array.isArray(buttonFor[entry.label]) - ? buttonFor[entry.label].some((button) => held.includes(button)) - : held.includes(buttonFor[entry.label]), - })).filter((entry) => entry.fade > .01); - // A held button lights only its newest appearance — not every earlier - // press of the same button still sitting in the buffer's history. - const claimed = new Set(); - for (let index = entries.length - 1; index >= 0; index--) { - const entry = entries[index]; - if (!entry.held) continue; - if (claimed.has(entry.label)) entry.held = false; - else claimed.add(entry.label); - } - if (!entries.length) return; - const lines = []; - let current = []; - let characters = 0; - for (const entry of entries) { - // A disc is a disc: pad rows wrap by count, not by how many letters the - // label would have spelled — eight buttons to a line, not four. - const cost = 1; - const nextLength = characters + (current.length ? 1 : 0) + cost; - if (current.length && nextLength > commandStreamColumns) { - lines.push(current); - current = []; - characters = 0; - } - current.push(entry); - characters += (current.length > 1 ? 1 : 0) + - 1; - } - if (current.length) lines.push(current); - // A deep buffer on a wide-text platform can outgrow the corner it lives in. - // Dropping whole rows off the top keeps the newest commands on screen rather - // than letting the block climb out of the safe area. - if (lines.length > commandStreamRows) - lines.splice(0, lines.length - commandStreamRows); - const safe = hudSafeRect(); - const size = Math.max(15, Math.round(hudTypeSize * .72)); - const lineHeight = Math.round(size * 1.5) + 5; - // Commands are notation, not player-state furniture: both read from the - // top edge, with P1 owning top-left and P2 mirrored so the lists cannot - // overwrite one another. - const firstY = safe.top + hudTypeSize * 3 + 18; - for (let row = 0; row < lines.length; row++) { - const lineEntries = lines[row]; - const width = lineEntries.reduce((sum, entry, index) => sum + - handleWidth(entry.text, size) + (index ? Math.round(size * .34) : 0), 0); - let cursor = side === 0 ? safe.left + 8 : safe.right - 8 - width; - const y = firstY + row * lineHeight; - for (const entry of lineEntries) { - const quiet = mixColor([104, 114, 136], [82, 90, 108], visualTheme.light); - const live = entry.held ? player.color : quiet; - const glyphInk = entry.fade >= 1 ? live - : mixColor(visualTheme.light > .5 ? [230, 239, 247] : [7, 8, 28], - live, entry.fade); - typeWrite(entry.text, cursor, y, size, ...glyphInk); - cursor += handleWidth(entry.text, size) + Math.round(size * .34); - } - } -} - -function drawFightIntro(introSeconds, titleInk, statusShadow) { - const centerX = viewCenterX(); - const centerY = (stageTop + stageBottom) / 2; - const nameSize = compactLayout() ? 48 : 74; - const drawCenteredName = (player) => { - const width = handleWidth(visibleHandle(player), nameSize); - drawFloatingHandle(player, centerX - width / 2, centerY - nameSize / 2, - nameSize); - }; - if (introSeconds < .65) { - drawCenteredName(players[0]); - return; - } - if (introSeconds < 1.3) { - drawCenteredName(players[1]); - return; - } - - const andText = "and"; - const andSize = nameSize * .72; - const firstWidth = handleWidth(visibleHandle(players[0]), nameSize); - const secondWidth = handleWidth(visibleHandle(players[1]), nameSize); - const andWidth = handleWidth(andText, andSize); - const pairGap = compactLayout() ? 320 - 30 : 320; - const pairWidth = firstWidth + pairGap + andWidth + pairGap + secondWidth; - const pairLeft = centerX - pairWidth / 2; - const pairStarts = [pairLeft, - pairLeft + firstWidth + pairGap + andWidth + pairGap]; - const travel = clamp((introSeconds - 1.3) / .6, 0, 1); - const eased = travel * travel * (3 - travel * 2); - for (let side = 0; side < players.length; side++) { - const player = players[side]; - const target = playerHandleLayout(player, side); - const startX = pairStarts[side]; - const startY = centerY - target.size / 2; - drawFloatingHandle(player, lerp(startX, target.x, eased), - lerp(startY, target.y, eased), lerp(nameSize, target.size, eased)); - } - const andY = centerY - andSize / 2; - if (introSeconds < 2.45) { - const andX = centerX - andWidth / 2; - typeWrite(andText, andX + 4, andY + 5, andSize, ...statusShadow); - typeWrite(andText, andX, andY, andSize, ...titleInk); - return; - } - const fightAge = clamp((introSeconds - 2.45) / .55, 0, 1); - const fightSize = lerp(72, 96, Math.sin(fightAge * Math.PI)); - const startText = "start"; - const startWidth = startText.length * fightSize * .58; - typeWrite(startText, centerX - startWidth / 2 + 5, - centerY - fightSize / 2 + 6, fightSize, ...statusShadow); - typeWrite(startText, centerX - startWidth / 2, - centerY - fightSize / 2, fightSize, ...titleInk); -} - -// A pole or a branch crossing the near plane has the same problem a face does, -// and the same cure — trim the segment at the plane rather than letting the -// far end pin itself to the near distance and rake across the screen. Null is -// a segment wholly behind the camera. -// Trimmed at the pin rather than the near plane: a line is handed to the host -// whole, with no band to catch it, so a pole crossing the plane would draw a -// streak the length of the guard band. Pulling the cut back to the pin costs -// nothing visible — nothing in the arena passes within eighty units of the -// lens — and keeps both ends of every line on the map. -function worldSegment(x1, y1, z1, x2, y2, z2) { - let a = cameraDoll.toView({ x: x1, y: y1, z: z1 }); - let b = cameraDoll.toView({ x: x2, y: y2, z: z2 }); - if (a.z < cameraPin && b.z < cameraPin) return null; - if (a.z < cameraPin) - a = mixVertex(a, b, (cameraPin - a.z) / (b.z - a.z)); - else if (b.z < cameraPin) - b = mixVertex(b, a, (cameraPin - b.z) / (a.z - b.z)); - const from = cameraDoll.projectView(a); - const to = cameraDoll.projectView(b); - return [from.x, from.y, from.z, to.x, to.y, to.z].every(Number.isFinite) - ? { from, to } : null; -} - -function worldLine(x1, y1, z1, x2, y2, z2, width, color) { - const segment = worldSegment(x1, y1, z1, x2, y2, z2); - if (!segment) return; - line(segment.from.x, segment.from.y, segment.to.x, segment.to.y, - width, ...color); -} - -function worldCapsule(x1, y1, z1, x2, y2, z2, width, color) { - const segment = worldSegment(x1, y1, z1, x2, y2, z2); - if (!segment) return; - const { from, to } = segment; - const previousDepth = triangleDepth; - triangleDepth = Math.min(from.z, to.z) - .004; - filledCapsule(from.x, from.y, to.x, to.y, width, color); - triangleDepth = previousDepth; -} - -function worldQuad(a, b, c, d, color) { - // Lighting is decided in world space, off the surface the quad names, so it - // is the same shade however the clipper ends up cutting the face up. - const ab = { x: b.x - a.x, y: b.y - a.y, z: b.z - a.z }; - const ac = { x: c.x - a.x, y: c.y - a.y, z: c.z - a.z }; - const normal = normalize3(cross3(ab, ac)); - const light = { x: -globalLight.x, y: -globalLight.y, z: -globalLight.z }; - const illumination = .72 + Math.max(0, dot3(normal, light)) * .28; - const lit = color.map((channel) => Math.round(channel * illumination)); - worldTriangle(a, b, c, lit); - worldTriangle(a, c, d, lit); -} - -function shadowSurfaceY(x, y) { - return PLATFORM && x >= platformLeft && x <= platformRight && y <= platformY + 1 - ? platformY : floorY; -} - -function drawSpotShadow(x, y, z, radius, color) { - const surfaceY = shadowSurfaceY(x, y); - const height = Math.max(0, surfaceY - y); - const reach = Math.min(520, height * .22); - const focus = clamp(1 - height / 5200, .42, 1); - const shadowX = x + globalLight.x * reach; - const shadowZ = z + globalLight.z * reach; - const center = projectPoint(shadowX, surfaceY - 2, shadowZ); - const edge = projectPoint(shadowX + radius, surfaceY - 2, shadowZ); - if (![center.x, center.y, edge.x, edge.y].every(Number.isFinite) || - [center.x, center.y, edge.x, edge.y].some((value) => Math.abs(value) > 30000)) - return; - const radiusX = Math.max(4, Math.abs(edge.x - center.x) * - (.5 + .18 * focus)); - const radiusY = Math.max(2, radiusX * (.2 + .08 * focus)); - // Bind the shadow to the owning object's depth, then bias it away from the - // camera. It remains above the terrain pass but can never win against the - // object that casts it. Restore the depth after — this used to leak, and - // everything drawn until the next assignment inherited the last shadow - // caster's depth: nondeterministic layering, frame to frame, on console. - const previousDepth = triangleDepth; - triangleDepth = projectPoint(x, y, z).z + .018; - const sides = 14; - for (let side = 0; side < sides; side++) { - const a = side * Math.PI * 2 / sides; - const b = (side + 1) * Math.PI * 2 / sides; - screenTriangle(center.x, center.y, - center.x + Math.cos(a) * radiusX, - center.y + Math.sin(a) * radiusY, - center.x + Math.cos(b) * radiusX, - center.y + Math.sin(b) * radiusY, ...color); - } - triangleDepth = previousDepth; -} - -function projectedBallRadius(ball) { - const center = projectPoint(ball.x, ball.y, ball.z); - const edge = projectPoint(ball.x + ball.radius, ball.y, ball.z); - return Math.max(8, Math.abs(edge.x - center.x)); -} - -function drawBall(ball) { - if (!ball.active) return; - const point = projectPoint(ball.x, ball.y, ball.z); - // A ball is a sphere in the orthographic world. Measuring its projected - // world radius keeps the screen hit shape circular while still respecting - // the perspective intro and loss cameras. - const radius = projectedBallRadius(ball); - if (!Number.isFinite(point.x) || !Number.isFinite(point.y) || - !Number.isFinite(radius) || radius > 12000 || point.x + radius < 0 || - point.x - radius > viewWidth() || point.y + radius < 0 || - point.y - radius > viewHeight) return; - const soccer = ball.type === "soccer"; - const beach = ball.type === "beach"; - const polygon = (x, y, polygonRadius, sides, rotation, color) => { - for (let side = 0; side < sides; side++) { - const a = rotation + side * Math.PI * 2 / sides; - const b = rotation + (side + 1) * Math.PI * 2 / sides; - screenTriangle(x, y, - x + Math.cos(a) * polygonRadius, - y + Math.sin(a) * polygonRadius, - x + Math.cos(b) * polygonRadius, - y + Math.sin(b) * polygonRadius, ...color); - } - }; - if (beach) { - const panels = [ - [244, 66, 96], [255, 146, 52], [255, 226, 66], [72, 210, 126], - [54, 196, 224], [70, 112, 238], [184, 82, 224], - ]; - const palettePhase = runtime().monotonicUs / 850000; - const paletteStep = Math.floor(palettePhase); - const paletteMix = palettePhase - paletteStep; - const sides = 16; - for (let side = 0; side < sides; side++) { - const a = ball.rotation + side * Math.PI * 2 / sides; - const b = ball.rotation + (side + 1) * Math.PI * 2 / sides; - // Native monotonic time can read negative, and a negative remainder - // would index off the front of the palette. - const panel = ((Math.floor(side / 2) + paletteStep) % panels.length + - panels.length) % panels.length; - const panelColor = mixColor(panels[panel], - panels[(panel + 1) % panels.length], paletteMix); - screenTriangle(point.x, point.y, - point.x + Math.cos(a) * radius * .92, - point.y + Math.sin(a) * radius * .92, - point.x + Math.cos(b) * radius * .92, - point.y + Math.sin(b) * radius * .92, - ...panelColor); - } - filledDisc(point.x, point.y, radius * .13, [246, 241, 220]); - return; - } - filledDisc(point.x, point.y, radius * .92, - soccer ? [226, 232, 224] : [232, 104, 28]); - const seam = [42, 31, 29]; - if (soccer) { - polygon(point.x, point.y, radius * .22, 5, - ball.rotation - Math.PI / 2, seam); - for (let patch = 0; patch < 5; patch++) { - const angle = ball.rotation + patch * Math.PI * 2 / 5; - const patchX = point.x + Math.cos(angle) * radius * .58; - const patchY = point.y + Math.sin(angle) * radius * .58; - filledCapsule(point.x + Math.cos(angle) * radius * .2, - point.y + Math.sin(angle) * radius * .2, - patchX, patchY, Math.max(1.5, radius * .035), seam); - polygon(patchX, patchY, radius * .13, 5, - angle - Math.PI / 2, seam); - } - } else { - const width = Math.max(2, radius * .075); - for (const angle of [ball.rotation, ball.rotation + Math.PI / 2]) { - const dx = Math.cos(angle) * radius * .84; - const dy = Math.sin(angle) * radius * .84; - filledCapsule(point.x - dx, point.y - dy, - point.x + dx, point.y + dy, width, seam); - } - for (const direction of [-1, 1]) { - let previous = null; - for (let step = -6; step <= 6; step++) { - const y = step / 6 * radius * .82; - const x = direction * Math.cos(step / 6 * Math.PI / 2) * radius * .42; - const next = { x: point.x + x, y: point.y + y }; - if (previous) filledCapsule(previous.x, previous.y, - next.x, next.y, width, seam); - previous = next; - } - } - filledDisc(point.x, point.y, Math.max(1.5, width * .55), seam); - } -} - -function drawBallHitboxes() { - if (!debugHitboxes) return; - for (const item of balls) { - if (!item.active) continue; - const point = projectPoint(item.x, item.y, item.z); - const radius = projectedBallRadius(item); - if (![point.x, point.y, radius].every(Number.isFinite) || radius > 5000 || - Math.abs(point.x) > 30000 || Math.abs(point.y) > 30000) continue; - filledRing(point.x, point.y, radius + 5, radius + 2, [58, 222, 255]); - } -} - -function drawGunPickup(pickup, t) { - if (!pickup.active) return; - const bobY = pickup.y + Math.sin(t * 3 + pickup.x * .001) * 8; - const scale = cameraScale(); - const metal = mixColor([202, 212, 228], [52, 59, 72], visualTheme.light); - const grip = mixColor([126, 106, 88], [40, 43, 52], visualTheme.light); - const barrelWidth = Math.max(2, 5 * scale); - const gripWidth = Math.max(2, 4 * scale); - // A handgun-sized world object in gunmetal and grip material, not a glowing - // pickup glyph with a second silhouette around it. - // Capsules, not line() — the native renderer buries the whole line - // stratum beneath the world's triangles, which is how the gun spent a day - // drawn under the floor on console while the web shell showed it fine. - worldCapsule(pickup.x - 16, bobY, pickup.z, - pickup.x + 20, bobY, pickup.z, barrelWidth, metal); - worldCapsule(pickup.x + 2, bobY + 1, pickup.z, - pickup.x + 8, bobY + 18, pickup.z, gripWidth, grip); -} - -function drawBullet(bullet) { - const point = projectPoint(bullet.x, bullet.y, bullet.z); - filledDisc(point.x, point.y, Math.max(3, 6 * cameraScale()), [8, 10, 16]); -} - -function drawGrenadePickup(pickup, t) { - if (!pickup.active) return; - const bobY = pickup.y + Math.sin(t * 3.2 + pickup.x * .001) * 8; - const point = projectPoint(pickup.x, bobY, pickup.z); - const scale = cameraScale(); - const shell = mixColor([166, 194, 112], [72, 96, 58], visualTheme.light); - const fuse = mixColor([210, 218, 232], [45, 50, 60], visualTheme.light); - const radius = Math.max(3, 9 * scale); - filledDisc(point.x, point.y, radius, shell); - worldCapsule(pickup.x + 1, bobY - 10, pickup.z, - pickup.x + 11, bobY - 20, pickup.z, Math.max(2, 3 * scale), fuse); -} - -function drawGrenade(grenade) { - const point = projectPoint(grenade.x, grenade.y, grenade.z); - if (grenade.exploding) { - const radius = grenade.blastRadius * cameraScale(); - const width = Math.max(3, 10 * cameraScale()); - filledRing(point.x, point.y, radius, - Math.max(0, radius - width), [255, 105, 105]); - return; - } - const blink = grenade.fuse < .45 && Math.floor(grenade.fuse * 20) % 2 === 0; - const color = blink ? [255, 255, 255] : players[grenade.owner].color; - filledDisc(point.x, point.y, Math.max(4, 22 * cameraScale()), color); - const tail = projectPoint(grenade.x - Math.sign(grenade.vx) * 90, - grenade.y - 14, grenade.z); - filledCapsule(tail.x, tail.y, point.x, point.y, - Math.max(2, 6 * cameraScale()), color); -} - -function drawWindFlag(t, color) { - const poleX = (platformLeft + platformRight) / 2; - const poleZ = 480; - const poleBottom = platformY; - const poleTop = platformY - 150; - const calm = windMph < .35; - const flagHeight = 68; - const length = calm ? 58 : 78 + windMph * 7; - const gust = calm ? 0 - : Math.sin(t * (4 + windMph * .16)) * (10 + windMph * .8); - const tipX = poleX + windDirection * length; - const tipY = poleTop + flagHeight * .5 + gust; - const width = Math.max(3, 13 * cameraScale()); - const poleInk = mixColor([214, 222, 236], [50, 57, 70], visualTheme.light); - const edgeInk = mixColor([242, 246, 252], [26, 31, 44], visualTheme.light); - const fabric = calm - ? mixColor([92, 205, 255], [35, 112, 190], visualTheme.light) : color; - const flagPoints = [ - projectPoint(poleX, poleTop, poleZ), - projectPoint(tipX, tipY, poleZ), - projectPoint(poleX, poleTop + flagHeight, poleZ), - ]; - if (flagPoints.every((point) => [point.x, point.y, point.z].every(Number.isFinite))) - projectedTriangle(flagPoints[0], flagPoints[1], flagPoints[2], fabric); - worldCapsule(poleX, poleBottom, poleZ, - poleX, poleTop, poleZ, width, poleInk); - const edgeWidth = Math.max(2, width * .34); - worldCapsule(poleX, poleTop, poleZ, - tipX, tipY, poleZ, edgeWidth, edgeInk); - worldCapsule(tipX, tipY, poleZ, - poleX, poleTop + flagHeight, poleZ, edgeWidth, edgeInk); - worldCapsule(poleX, poleTop + flagHeight, poleZ, - poleX, poleTop, poleZ, edgeWidth, edgeInk); - worldCapsule(poleX - 35, poleBottom, poleZ, - poleX + 35, poleBottom, poleZ, width, poleInk); -} - -function hashUnit(text) { - let hash = 2166136261; - for (let cursor = 0; cursor < text.length; cursor++) { - hash ^= text.charCodeAt(cursor); - hash = Math.imul(hash, 16777619); - } - return (hash >>> 0) / 4294967295; -} - -function airSeedValue(index, channel = 0) { - return hashUnit("oskiewar-air:" + index + ":" + channel); -} - -function resetAirParticles() { - const count = 18; - const spanX = worldRight - worldLeft; - const spanY = floorY - ceilingY; - const spanZ = worldFar - worldNear; - airParticles.length = 0; - for (let index = 0; index < count; index++) - airParticles.push({ - id: "air:" + index, - kind: "air", - position: { - x: worldLeft + airSeedValue(index, 1) * spanX, - y: ceilingY + 180 + airSeedValue(index, 2) * (spanY - 360), - z: worldNear + 100 + airSeedValue(index, 3) * (spanZ - 200), - }, - velocity: { x: 0, y: 0, z: 0 }, - radius: 8 + airSeedValue(index, 4) * 14, - phase: airSeedValue(index, 5) * Math.PI * 2, - }); -} - -// A stream-function curl gives the air a divergence-free-looking circulation -// instead of independent sine wiggles. Entities relax toward this velocity, -// advect in fixed simulation time, and wrap at arena boundaries. -function airFlowAt(position, seconds, phase) { - const nx = (position.x - worldLeft) / (worldRight - worldLeft) * Math.PI * 2; - const ny = (position.y - ceilingY) / (floorY - ceilingY) * Math.PI * 2; - const age = seconds * .32 + phase; - const circulation = 96; - return { - x: windDirection * (42 + windMph * 12) + - circulation * Math.sin(nx + age) * Math.cos(ny - age * .7), - y: -circulation * Math.cos(nx + age) * Math.sin(ny - age * .7), - z: 44 * Math.sin(nx + ny + age * .55), - }; -} - -const wrapWorld = (value, low, high) => { - const span = high - low; - return low + ((value - low) % span + span) % span; -}; - -function simulateAirParticles(dt, now) { - if (!airParticles.length) resetAirParticles(); - const seconds = now / 1000000; - const response = 1 - Math.exp(-dt * 2.8); - for (const entity of airParticles) { - const flow = airFlowAt(entity.position, seconds, entity.phase); - entity.velocity.x = lerp(entity.velocity.x, flow.x, response); - entity.velocity.y = lerp(entity.velocity.y, flow.y, response); - entity.velocity.z = lerp(entity.velocity.z, flow.z, response); - entity.position.x = wrapWorld(entity.position.x + entity.velocity.x * dt, - worldLeft, worldRight); - entity.position.y = wrapWorld(entity.position.y + entity.velocity.y * dt, - ceilingY + 120, floorY - 120); - entity.position.z = wrapWorld(entity.position.z + entity.velocity.z * dt, - worldNear + 80, worldFar - 80); - } -} - -function drawAmbientMotes(color) { - const previousDepth = triangleDepth; - for (const entity of airParticles) { - const { x, y, z } = entity.position; - const point = projectPoint(x, y, z); - const edge = projectPoint(x + entity.radius, y, z); - const radius = Math.max(.8, Math.hypot(edge.x - point.x, edge.y - point.y)); - const depthAmount = (z - worldNear) / (worldFar - worldNear); - const ink = mixColor([18, 24, 48], color, .24 + (1 - depthAmount) * .34); - if (![point.x, point.y, point.z, radius].every(Number.isFinite) || - point.x < stageLeft - radius || point.x > stageRight + radius || - point.y < -radius || point.y > viewHeight + radius) continue; - triangleDepth = point.z; - filledDisc(point.x, point.y, radius, ink); - } - triangleDepth = previousDepth; -} - -function drawSelectPortrait(player, x, y, scale, t) { - const color = player.color; - const head = { x, y: y - 130 * scale, radius: 34 * scale }; - filledDisc(head.x, head.y, head.radius + Math.max(2, 3 * scale), [8, 12, 24]); - filledDisc(head.x, head.y, head.radius, color); - line(x, y - 94 * scale, x, y + 20 * scale, 12 * scale, ...color); - line(x, y - 65 * scale, x - 62 * scale, y - 8 * scale, 10 * scale, ...color); - line(x, y - 65 * scale, x + 62 * scale, y - 8 * scale, 10 * scale, ...color); - line(x, y + 20 * scale, x - 48 * scale, y + 112 * scale, 11 * scale, ...color); - line(x, y + 20 * scale, x + 48 * scale, y + 112 * scale, 11 * scale, ...color); - drawFace(player, head, contrastShadow(color), t); -} - -function selectionPreview(option) { - return { ...players[1], name: option.fighter.handle, - color: option.fighter.color, - npc: option.kind === "dummy" || option.kind === "bot", - bot: option.kind === "bot" }; -} - -function drawSelectionScreen(t, ink, panel) { - const layout = selectionTouchLayout(); - const hover = selectionHover(layout); - if (globalThis.__oskiewarTouch) globalThis.__oskiewarTouch.hover = hover; - const compact = compactLayout(); - const title = selectionStep === 0 ? "pick your pal" : "who are you fighting?"; - const titleSize = compact ? 36 : 58; - const titleY = compact ? 58 : 86; - const titleWidth = handleWidth(title, titleSize); - typeWrite(title, viewCenterX() - titleWidth / 2, titleY, titleSize, ...ink); - const stepText = (selectionStep + 1) + "/2"; - const stepSize = compact ? 24 : 32; - typeWrite(stepText, layout.back.x + layout.back.width + (compact ? 18 : 28), - layout.back.y + (compact ? 11 : 18), stepSize, - ...mixColor([140, 150, 185], ink, .35)); - const backHovered = hover?.back; - const options = selectionOptions(); - // Neighbours first so the focused card always lands on top of them. - const wheel = [...layout.options].sort((a, b) => - Math.abs(b.slot) - Math.abs(a.slot)); - for (const rect of wheel) { - const option = options[rect.index]; - const player = selectionPreview(option); - const disabled = Boolean(option.disabled); - const hovered = !disabled && hover?.option === rect.index; - const focused = !disabled && rect.slot === 0; - const disabledInk = mixColor([74, 80, 96], [164, 168, 178], - visualTheme.light); - const optionColor = disabled ? disabledInk : option.fighter.color; - if (hovered || focused) - box(rect.x, rect.y + rect.height - (hovered ? 8 : 5), rect.width, - hovered ? 8 : 5, ...optionColor); - const scale = rect.height / 520 * 1.05; - const portraitY = rect.y + rect.height * (compact ? .55 : .53); - drawSelectPortrait(player, rect.x + rect.width / 2, portraitY, scale, t); - const label = option.fighter.handle.toLowerCase(); - const labelSize = Math.max(16, Math.round(rect.width * .085)); - const labelWidth = handleWidth(label, labelSize); - drawHandle(label, rect.x + (rect.width - labelWidth) / 2, - rect.y + rect.height - labelSize - (compact ? 10 : 18), labelSize, - disabled ? [disabledInk] : option.fighter.colors, optionColor); - } - // Chevrons inside the focused card name the gesture that turns the wheel, - // without crowding the neighbours they point at. - if (layout.options.length > 1) { - const focus = layout.options.find((rect) => rect.slot === 0); - const arrowSize = compact ? 30 : 52; - const arrowY = focus.y + focus.height / 2 - arrowSize / 2; - const inset = (compact ? 8 : 14) + Math.sin(t * 3.4) * (compact ? 3 : 6); - typeWrite("<", focus.x + inset, arrowY, arrowSize, ...ink); - typeWrite(">", focus.x + focus.width - inset - - comicGlyphAdvance(">", arrowSize), arrowY, arrowSize, ...ink); - } - // Keep navigation above every option on narrow/tall browser layouts. - box(layout.back.x, layout.back.y, layout.back.width, layout.back.height, - ...mixColor(panel, ink, backHovered ? .25 : .08)); - typeWrite("< back", layout.back.x + (compact ? 12 : 20), - layout.back.y + (compact ? 10 : 17), compact ? 25 : 34, ...ink); - const controls = selectionControlKeys(); - drawCenteredKeycapRun(controls, viewHeight - (compact ? 52 : 66), - compact ? 15 : 20, inputPads[0]?.down || [], ink); -} - -const titlePaletteNight = [ - [255, 105, 190], [111, 232, 210], [255, 232, 92], [130, 150, 255], - [255, 126, 92], [188, 128, 255], [92, 205, 255], [246, 248, 255], -]; -const titlePaletteDay = [ - [190, 32, 118], [0, 126, 124], [176, 112, 0], [73, 82, 190], - [204, 59, 43], [114, 68, 185], [24, 107, 181], [47, 56, 82], -]; - -function animatedTitleColor(index, t, daylight = visualTheme.light) { - const count = titlePaletteNight.length; - // Native monotonic time can begin a fraction before startedAt settles. - // Normalize the remainder so that first frame still lands in the palette. - const phase = ((index + t * .42) % count + count) % count; - const from = Math.floor(phase); - const amount = phase - from; - const eased = amount * amount * (3 - amount * 2); - const night = mixColor(titlePaletteNight[from], - titlePaletteNight[(from + 1) % count], eased); - const day = mixColor(titlePaletteDay[from], - titlePaletteDay[(from + 1) % count], eased); - return mixColor(night, day, daylight); -} - -// The dummy count, as a sentence. The host hands over a window and a total; -// the window is already the shortest one that had anything in it, so all that -// is left here is to say it in English. Zero returns nothing at all — an -// arcade does not advertise that nobody played today. -function dummyPopLine(stat) { - const pops = Math.floor(Number(stat?.pops) || 0); - const hours = Math.floor(Number(stat?.hours) || 0); - if (pops <= 0 || hours <= 0 || hours > 48) return ""; - return pops + " dummy head" + (pops === 1 ? "" : "s") + " popped in the last " + - (hours === 1 ? "hour" : hours + " hours"); -} - -function drawDummyPopLine(button, transitionInk) { - // Not on the poster: the social preview is burned to a file whose hash is - // checked at deploy, and a number that moves would make it stale hourly. - if (typeof capabilities !== "function" || - capabilities().socialPreview === true) return; - const line = dummyPopLine(capabilities().pops); - if (!line) return; - const size = Math.round(hudTypeSize * (compactLayout() ? .46 : .56)); - const width = handleWidth(line, size); - const x = viewCenterX() - width / 2; - const y = button.y + button.height + (compactLayout() ? 14 : 22); - if (y + size > viewHeight - 8) return; - const ink = transitionInk || - mixColor([198, 206, 232], [58, 70, 104], visualTheme.light); - typeWrite(line, x + 2, y + 2, size, - ...mixColor([8, 10, 26], [226, 234, 246], visualTheme.light)); - typeWrite(line, x, y, size, ...ink); -} - -function titleButtonRect() { - const compact = compactLayout(); - const textSize = hudTypeSize; - const textWidth = handleWidth("start", textSize); - const width = textWidth + (compact ? 54 : 76); - const height = textSize + (compact ? 24 : 34); - return { x: viewCenterX() - width / 2, - y: viewHeight * (compact ? .61 : .64) - (compact ? 10 : 15), - width, height, textSize, textWidth }; -} - -// A letter you can push around. `grow` eases toward 1 while the pointer is on -// it and `kick` is a decaying shudder fired the frame it is first touched, so -// the word answers one letter at a time instead of inflating as a block. -function toyGlyph(toys, index, hot, dt) { - const toy = toys[index] || (toys[index] = { grow: 0, kick: 0, hot: false }); - toy.grow += ((hot ? 1 : 0) - toy.grow) * (1 - Math.exp(-dt * (hot ? 16 : 7))); - if (hot && !toy.hot) toy.kick = 1; - toy.hot = hot; - toy.kick *= Math.exp(-dt * 5.5); - return toy; -} - -const pointInCell = (point, x, y, width, height) => point && x <= point.x && - point.x <= x + width && y <= point.y && point.y <= y + height; - -// Screen-space outline, four boxes, no projection — safe anywhere. -function strokeBox(x, y, width, height, thickness, color) { - box(x, y, width, thickness, ...color); - box(x, y + height - thickness, width, thickness, ...color); - box(x, y, thickness, height, ...color); - box(x + width - thickness, y, thickness, height, ...color); -} - -function drawTitleScreen(t, ink, transitionAge = -1) { - const compact = compactLayout(); - const socialPreview = typeof capabilities === "function" && - capabilities().socialPreview === true; - const title = "oskiewar"; - const breath = 1 + Math.sin(t * .9) * .018; - const socialTitleSize = Math.min(220, - (stageRight - stageLeft - 56) / handleWidth(title, 1)); - const titleSize = (socialPreview ? socialTitleSize : compact ? 88 : 154) * breath; - const titleWidth = handleWidth(title, titleSize); - const titleX = viewCenterX() - titleWidth / 2; - const titleY = viewHeight * (compact ? .38 : .35); - - // Air fuzzies: sparse motes drifting the whole frame, no panels or stripes - // behind the wordmark. - if (transitionAge < 0) - for (let index = 0; index < (compact ? 10 : 16); index++) { - const phase = index * 2.39996; - const x = stageLeft + (stageRight - stageLeft) * - (.5 + .47 * Math.sin(t * (.055 + index % 5 * .01) + phase)); - const y = viewHeight * - (.5 + .45 * Math.cos(t * (.043 + index % 4 * .012) + phase * 1.7)); - const radius = (compact ? 2 : 3) + (index % 3); - circle(x, y, radius, Math.max(1.5, radius * .48), - animatedTitleColor(index, t * .7)); - } - - let cursor = titleX; - // Kerning scaffold: one cell per glyph, taken from the same advance the - // layout walks, so drift and bob read against their own metrics. - const glyphCells = debugHitboxes && transitionAge < 0 && !socialPreview - ? [] : null; - const flashPalette = [[255, 226, 48], [70, 224, 92], [181, 255, 48]]; - const flash = transitionAge >= 0 - ? flashPalette[Math.floor(transitionAge / .065) % flashPalette.length] : null; - const fade = transitionAge >= 0 ? clamp((transitionAge - .46) / .24, 0, 1) : 0; - const transitionInk = flash ? mixColor(flash, [7, 10, 26], fade) : null; - // Toys ease in real time, and the burn renders headless with no pointer, so - // the poster stays the same picture it always was. - const dt = titleToyAt < 0 ? 0 : clamp(t - titleToyAt, 0, .1); - titleToyAt = t; - const raw = globalThis.__oskiewarTouch?.pointer; - const pointer = transitionAge < 0 && !socialPreview && raw?.active && - Number.isFinite(raw.x) && Number.isFinite(raw.y) ? raw : null; - let held = -1; - for (let index = 0; index < title.length; index++) { - const character = title[index]; - const bob = Math.sin(t * 2.05 + index * .72) * (compact ? 5 : 8); - const drift = Math.cos(t * 1.12 + index * .91) * (compact ? 3 : 5) + - Math.sin(t * .63 + index * 1.71) * (compact ? 7 : 12); - const advance = comicGlyphAdvance(character, titleSize); - // The cell is what the pointer touches, so a letter that has swollen out - // of it cannot chase the cursor or shove its neighbours along the line. - const hot = pointInCell(pointer, cursor + drift, titleY + bob, - advance, titleSize); - if (hot) held = index; - const toy = toyGlyph(titleToys, index, hot, dt); - const size = titleSize * - (1 + toy.grow * .38 + toy.kick * Math.sin(t * 31 + index) * .17); - const shudder = toy.kick * Math.sin(t * 27 + index * 1.3) * - (compact ? 6 : 11); - typeWrite(character, - cursor + drift + (advance - comicGlyphAdvance(character, size)) / 2 + - shudder, - titleY + bob - (size - titleSize) * .5 - toy.grow * (compact ? 6 : 11), - size, ...(transitionInk || animatedTitleColor(index, t))); - if (glyphCells) glyphCells.push([cursor + drift, titleY + bob, advance]); - cursor += advance; - } - if (glyphCells) { - strokeBox(titleX, titleY, titleWidth, titleSize, 2, [92, 132, 255]); - for (const [x, y, advance] of glyphCells) { - strokeBox(x, y, advance, titleSize, 2, [255, 92, 116]); - box(x, y, 2, titleSize, 116, 255, 184); - } - typeWrite("size " + Math.round(titleSize) + " width " + - Math.round(titleWidth), titleX, titleY + titleSize + 12, 24, - 116, 255, 184); - } - - const prompt = "start"; - const button = titleButtonRect(); - const hovered = Boolean(pointer) && pointInRect(pointer, button); - if (globalThis.__oskiewarTouch) { - globalThis.__oskiewarTouch.titleButton = button; - globalThis.__oskiewarTouch.titleHover = hovered; - globalThis.__oskiewarTouch.titleGlyph = held; - } - promptBounce += ((hovered ? 1 : 0) - promptBounce) * - (1 - Math.exp(-dt * (hovered ? 14 : 6))); - const promptPulse = .68 + (Math.sin(t * 3.2) + 1) * .16; - const promptInk = transitionInk || - mixColor([196, 142, 18], [255, 238, 82], promptPulse); - // The word floats on its own; the rect survives only as the touch target. - // Yellow type on a bright sky needs an edge, so a sharp offset shadow - // deepens as the background lightens and lifts as it goes to night. - if (!socialPreview) { - let promptCursor = button.x + (button.width - button.textWidth) / 2; - const promptY = button.y + (button.height - button.textSize) / 2 - 2; - const offset = Math.max(3, Math.round(button.textSize * .1)); - const shadowInk = mixColor([10, 12, 30], [86, 26, 116], visualTheme.light); - const litInk = hovered - ? mixColor(promptInk, [255, 255, 255], .35) : promptInk; - for (let index = 0; index < prompt.length; index++) { - const character = prompt[index]; - const advance = comicGlyphAdvance(character, button.textSize); - const toy = toyGlyph(promptToys, index, - pointInCell(pointer, promptCursor, promptY, advance, button.textSize), - dt); - // The whole word takes the bounce, but each letter takes it a beat after - // the one before, so start reads as a hop travelling along the word. - const hop = promptBounce * button.textSize * .3 * - Math.abs(Math.sin(t * 5.4 - index * .62)); - const size = button.textSize * (1 + toy.grow * .2 + toy.kick * .12); - const x = promptCursor + (advance - comicGlyphAdvance(character, size)) / 2; - const y = promptY - hop - (size - button.textSize) * .5; - if (!transitionInk) - typeWrite(character, x + offset, y + offset, size, ...shadowInk); - typeWrite(character, x, y, size, ...litInk); - promptCursor += advance; - } - drawDummyPopLine(button, transitionInk); - } - // Touch play keeps its thumbs in the bottom corners, and the fight is live - // under this screen now, so the stamp yields the pad rather than sit on it. - if (transitionAge >= 0 || socialPreview || (typeof capabilities === - "function" && capabilities().inputFamily === "touch")) return; - const titleUnixMs = runtime().unixMs || Date.now(); - const stamp = buildTimestamp.match(/^(\d{4})\.(\d{2})\.(\d{2})\.(\d{2})(\d{2})/); - const version = stamp - ? "build " + stamp[2] + "." + stamp[3] + " " + stamp[4] + ":" + stamp[5] - : "build " + buildTimestamp; - // Build provenance belongs to the title itself. Center it directly beneath - // the wordmark so the top-left remains the persistent input legend and live - // command score. - const safe = hudSafeRect(); - const versionSize = Math.round(hudTypeSize * .62); - typeWrite(version, viewCenterX() - handleWidth(version, versionSize) / 2, - titleY + titleSize + 18, versionSize, ...ink); - if (debugHitboxes) { - const fpsLabel = Math.round(displayFps || 0) + " fps"; - typeWrite(fpsLabel, viewCenterX() - handleWidth(fpsLabel, versionSize) / 2, - titleY + titleSize + versionSize + 26, - versionSize, ...ink); - } - const clock = hudClockBox(titleUnixMs); - typeWrite(clock.label, clock.left, safe.top + 2, clock.size, ...ink); - drawHudStatusTray(clock, ink, titleUnixMs); -} - -function pacificTimeLabel(unixMs) { - const date = new Date(unixMs); - const year = date.getUTCFullYear(); - const nthSunday = (month, occurrence, hour) => { - const first = new Date(Date.UTC(year, month, 1)); - const day = 1 + ((7 - first.getUTCDay()) % 7) + (occurrence - 1) * 7; - return Date.UTC(year, month, day, hour); - }; - const daylight = unixMs >= nthSunday(2, 2, 10) && - unixMs < nthSunday(10, 1, 9); - const zone = daylight ? "PDT" : "PST"; - const local = new Date(unixMs - (daylight ? 7 : 8) * 3600000); - const hour = local.getUTCHours(); - const minute = String(local.getUTCMinutes()).padStart(2, "0"); - return String(hour % 12 || 12) + ":" + minute + (hour < 12 ? "am " : "pm ") + zone; -} - -// Status indicators share one lane rather than each finding its own corner, so -// they read as a set: same cell, same row, in the title-safe frame just left of -// the wall clock. The lane is right-aligned because the clock and the round QR -// grow leftward from the same edge. -const statusGap = 6; -const statusCellSize = () => shellMode === "GAME" && debugHitboxes ? 56 : 26; - -function hudStatusIcons() { - const icons = []; - if (typeof capabilities === "function" && capabilities().midi) icons.push("midi"); - return icons; -} - -// The clock's footprint is computed once and shared, so the lane cannot drift -// out of step with where the clock actually lands. -function hudClockBox(unixMs) { - const safe = hudSafeRect(); - const label = pacificTimeLabel(unixMs); - const size = hudTypeSize; - const qrBox = spectatorQrBox(); - const right = qrBox ? qrBox.left - 14 : safe.right; - return { label, size, right, left: right - handleWidth(label, size) }; -} - -function hudStatusTray(clock) { - const icons = hudStatusIcons(); - if (!icons.length) return null; - const statusCell = statusCellSize(); - const right = clock.left - 14; - const width = icons.length * statusCell + (icons.length - 1) * statusGap; - return { icons, right, left: right - width, top: hudSafeRect().top, - height: statusCell }; -} - -// A one-octave keyboard: seven equal white keys with the black keys only on the -// boundaries that have them, so it reads as a piano at this size instead of an -// approximate smear. It brightens for a beat whenever a note leaves. -function drawStatusPiano(x, y, lit) { - const width = 21, height = 14, key = width / 7; - const left = Math.round(x - width / 2), top = Math.round(y - height / 2); - box(left, top, width, height, ...(lit ? [108, 240, 168] : [176, 184, 202])); - for (const step of [1, 2, 4, 5, 6]) - box(Math.round(left + step * key) - 1, top, 2, Math.round(height * .58), - 23, 27, 40); -} - -function drawHudStatusTray(clock, ink, unixMs) { - const tray = hudStatusTray(clock); - const statusCell = statusCellSize(); - // Debug is global state, so its large indicator owns bottom-center rather than - // masquerading as another peripheral in the clock-side status tray. - if (debugHitboxes) { - const safe = hudSafeRect(); - const pad = 3; - const top = safe.bottom - statusCell; - const left = viewCenterX() - statusCell / 2; - box(left - pad, top - pad, statusCell + pad * 2, - statusCell + pad * 2, 38, 44, 62); - drawDebugBug(viewCenterX(), top + statusCell / 2 + 2, - statusCell / 26); - } - if (!tray) return; - const lit = typeof capabilities === "function" && - unixMs - (capabilities().midiPulse || 0) < 140; - tray.icons.forEach((name, index) => { - const x = tray.left + index * (statusCell + statusGap) + statusCell / 2; - const y = tray.top + tray.height / 2; - if (name === "midi") drawStatusPiano(x, y, lit); - }); -} - -function drawDebugBug(x, y, scale = 1) { - const shell = [255, 86, 126]; - const detail = [22, 12, 34]; - filledDisc(x, y + 2 * scale, 8 * scale, shell); - filledDisc(x, y - 6 * scale, 5 * scale, shell); - line(x, y - 7 * scale, x - 6 * scale, y - 13 * scale, 2 * scale, ...shell); - line(x, y - 7 * scale, x + 6 * scale, y - 13 * scale, 2 * scale, ...shell); - line(x - 5 * scale, y, x - 11 * scale, y - 4 * scale, 2 * scale, ...shell); - line(x + 5 * scale, y, x + 11 * scale, y - 4 * scale, 2 * scale, ...shell); - line(x - 5 * scale, y + 5 * scale, x - 11 * scale, y + 9 * scale, 2 * scale, ...shell); - line(x + 5 * scale, y + 5 * scale, x + 11 * scale, y + 9 * scale, 2 * scale, ...shell); - line(x, y - scale, x, y + 9 * scale, 2 * scale, ...detail); - filledDisc(x - 2 * scale, y - 7 * scale, 1.2 * scale, detail); - filledDisc(x + 2 * scale, y - 7 * scale, 1.2 * scale, detail); -} - -// The round QR owns the top-right corner whenever it is up, so the HUD clock -// asks for its footprint before choosing a lane. -function spectatorQrBox() { - if (typeof capabilities === "function" && capabilities().socialPreview) - return null; - if (shellMode === "GAME" && !roundIsTimed()) return null; - if (!spectatorQr || typeof spectatorQr.getModuleCount !== "function") - return null; - const safe = hudSafeRect(); - const count = spectatorQr.getModuleCount(); - const quiet = 2; - const cell = Math.max(2, - Math.floor((compactLayout() ? 108 : 158) / (count + quiet * 2))); - const size = (count + quiet * 2) * cell; - return { left: safe.right - size, top: safe.top, size, cell, count, quiet }; -} - -function drawSpectatorQr(ink) { - if (typeof capabilities === "function" && capabilities().socialPreview) return; - const safe = hudSafeRect(); - const compact = compactLayout(); - const metaSize = compact ? Math.max(20, hudTypeSize * .58) : hudTypeSize; - if (debugHitboxes && shellMode === "GAME") { - const run = runtime(); - const measuredHz = Number(run.refreshHz) || displayFps || 0; - const aa = Math.max(1, Math.round(Number(run.antialiasingSamples) || 1)); - const aaLabel = String(run.antialiasingMode || (aa + "x")); - const surfaceLabel = Math.round(Number(run.width) || stageRight) + "x" + - Math.round(Number(run.height) || viewHeight) + " @ " + - measuredHz.toFixed(1) + "hz aa " + aaLabel; - const timingLabel = "frame " + (Number(run.frameMs) || 0).toFixed(2) + - "ms render " + (Number(run.renderCpuMs) || 0).toFixed(2) + - "ms present " + (Number(run.presentMs) || 0).toFixed(2) + "ms"; - typeWrite(surfaceLabel, safe.left + 2, safe.top + 2, metaSize, ...ink); - typeWrite(timingLabel, safe.left + 2, safe.top + metaSize + 5, - Math.max(18, metaSize * .72), ...ink); - } - const qr = spectatorQrBox(); - if (!qr) return; - const { count, quiet, cell, size, left, top } = qr; - const shadow = [24, 26, 34]; - const previousDepth = triangleDepth; - triangleDepth = -1.43; - screenRect(left + 3, top + 3, size, size, shadow); - screenRect(left, top, size, size, [250, 250, 247]); - // Dark modules coalesce into horizontal runs so a full code stays a few - // dozen faces instead of a few hundred — drawErrorQr already draws this way. - const dark = [7, 8, 14]; - for (let row = 0; row < count; row++) { - let run = 0; - for (let column = 0; column <= count; column++) { - if (column < count && spectatorQr.isDark(row, column)) { run++; continue; } - if (run) screenRect(left + (column - run + quiet) * cell, - top + (row + quiet) * cell, run * cell, cell, dark); - run = 0; - } - } - triangleDepth = previousDepth; -} - -// Framing marks rather than a full box: the corners state the bounds without -// drawing four lines through the fight. -function drawCornerCrops(rect, reach, width, color) { - const arm = Math.min(reach, (rect.right - rect.left) / 3, - (rect.bottom - rect.top) / 3); - for (const [x, towardX] of [[rect.left, 1], [rect.right, -1]]) - for (const [y, towardY] of [[rect.top, 1], [rect.bottom, -1]]) { - filledCapsule(x, y, x + towardX * arm, y, width, color); - filledCapsule(x, y, x, y + towardY * arm, width, color); - } -} - -function drawImpacts() { - for (const impact of impacts) { - const point = projectPoint(impact.x, impact.y, impact.z || 0); - const radius = (30 + (1 - impact.life / impact.duration) * - (impact.explosion ? 420 : impact.death ? 260 : 100)) * cameraScale(); - if (![point.x, point.y, radius].every(Number.isFinite) || radius > 10000 || - Math.abs(point.x) + radius > 30000 || Math.abs(point.y) + radius > 30000) - continue; - filledCapsule(point.x - radius, point.y, point.x + radius, point.y, - 5, [255, 255, 255]); - filledCapsule(point.x, point.y - radius, point.x, point.y + radius, - 5, [255, 255, 255]); - filledCapsule(point.x - radius * .7, point.y - radius * .7, - point.x + radius * .7, point.y + radius * .7, 4, [255, 232, 92]); - filledCapsule(point.x + radius * .7, point.y - radius * .7, - point.x - radius * .7, point.y + radius * .7, 4, [255, 105, 190]); - } -} - -function drawDetachedPart(fragment) { - const first = projectPoint(fragment.x1, fragment.y1, fragment.z1); - const second = projectPoint(fragment.x2, fragment.y2, fragment.z2); - const width = Math.max(2, fragment.width * cameraScale()); - const values = [first.x, first.y, second.x, second.y, width]; - if (!values.every(Number.isFinite) || width > 2000) return; - const margin = width + 4; - if (Math.abs(first.x) + margin > 30000 || - Math.abs(first.y) + margin > 30000 || - Math.abs(second.x) + margin > 30000 || - Math.abs(second.y) + margin > 30000 || - Math.hypot(second.x - first.x, second.y - first.y) > 30000) return; - filledCapsule(first.x, first.y, second.x, second.y, width + 3, [9, 12, 22]); - filledCapsule(first.x, first.y, second.x, second.y, width, fragment.color); -} - -function drawSafeZones() { - if (!debugHitboxes) return; - drawCornerCrops(hudSafeRect(), 46, 3, [255, 214, 84]); - drawCornerCrops(actionSafeRect(), 34, 2, [105, 255, 118]); -} - -function gamePaint() { - syncGameView(); - const run = runtime(); - if (globalThis.__oskiewarTouch) { - globalThis.__oskiewarTouch.screen = shellMode === "MENU" - ? titleTransitionAt !== null ? "title-transition" : "title" - : selecting ? "select" : "game"; - } - if (lastPaintAt > 0 && run.monotonicUs > lastPaintAt) { - const sample = clamp(1000000 / (run.monotonicUs - lastPaintAt), 1, 240); - displayFps = displayFps ? lerp(displayFps, sample, .12) : sample; - } - lastPaintAt = run.monotonicUs; - const t = (run.monotonicUs - startedAt) / 1000000; - if (typeof ac === "function") acFeed = ac(); - for (const player of players) player.handleColors = fighterProfile(player.name).colors; - visualTheme = displayTheme(); - triangleDepth = -1.4; - const skyDay = mixColor([176, 215, 245], [255, 160, 112], - visualTheme.sunset * .7); - const sky = mixColor([7, 8, 28], skyDay, visualTheme.light); - // Match the clear color to the arena sky. Camera framing can reveal the - // clear layer during a jump; a different clear color looked like a flash. - const outside = sky; - const arenaDay = mixColor([230, 239, 247], skyDay, visualTheme.sunset * .55); - const arena = mixColor([10, 13, 30], arenaDay, visualTheme.light); - const ground = mixColor([14, 19, 31], [183, 194, 185], visualTheme.light); - const platformColor = mixColor([24, 29, 46], [211, 198, 171], - visualTheme.light); - const titleInk = mixColor([245, 248, 255], [24, 35, 72], visualTheme.light); - const statusShadow = contrastShadow(titleInk); - const menuArena = mixColor([7, 10, 26], [235, 241, 248], visualTheme.light); - const menuPanel = mixColor([20, 28, 56], [215, 225, 239], visualTheme.light); - const menuInk = mixColor([245, 248, 255], [24, 35, 72], visualTheme.light); - wipe(...outside); - if (PAL_SELECT && selecting) { - box(0, 0, viewWidth(), viewHeight, ...menuArena); - drawSelectionScreen(t, menuInk, menuPanel); - return; - } - const cinematicAge = deathCinematicAge(run.monotonicUs); - if (cinematicAge < 0 || cinematicAge >= 1.45) containFighters(t); - cameraDoll.prepare(); - const { left: spanLeft, right: spanRight, - top: spanTop, bottom: spanBottom } = terrainSpan(); - worldQuad( - { x: spanLeft, y: spanTop, z: worldFar }, - { x: spanRight, y: spanTop, z: worldFar }, - { x: spanRight, y: spanBottom, z: worldFar }, - { x: spanLeft, y: spanBottom, z: worldFar }, sky); - worldQuad( - { x: spanLeft, y: floorY, z: worldNear }, - { x: spanRight, y: floorY, z: worldNear }, - { x: spanRight, y: floorY, z: worldFar }, - { x: spanLeft, y: floorY, z: worldFar }, ground); - const boundary = mixColor([12, 17, 38], arena, .72); - worldQuad( - { x: spanLeft, y: ceilingY, z: worldNear }, - { x: spanLeft, y: ceilingY, z: worldFar }, - { x: spanRight, y: ceilingY, z: worldFar }, - { x: spanRight, y: ceilingY, z: worldNear }, boundary); - worldQuad( - { x: worldLeft, y: spanTop, z: worldNear }, - { x: worldLeft, y: spanBottom, z: worldNear }, - { x: worldLeft, y: spanBottom, z: worldFar }, - { x: worldLeft, y: spanTop, z: worldFar }, boundary); - worldQuad( - { x: worldRight, y: spanTop, z: worldFar }, - { x: worldRight, y: spanBottom, z: worldFar }, - { x: worldRight, y: spanBottom, z: worldNear }, - { x: worldRight, y: spanTop, z: worldNear }, boundary); - const platformNear = -520; - const platformFar = 520; - const ledgeLeft = Math.max(platformLeft, spanLeft); - const ledgeRight = Math.min(platformRight, spanRight); - // The platform wants to be a slab, but the stage paints in order with no - // depth buffer — the volume's faces landed over the fighters on console. - // Back to the plane until the volume can be drawn behind them properly. - if (PLATFORM && ledgeLeft < ledgeRight) worldQuad( - { x: ledgeLeft, y: platformY, z: platformNear }, - { x: ledgeRight, y: platformY, z: platformNear }, - { x: ledgeRight, y: platformY, z: platformFar }, - { x: ledgeLeft, y: platformY, z: platformFar }, platformColor); - const shadowInk = mixColor([3, 5, 14], [92, 99, 101], - visualTheme.light * .72); - for (const player of players) - if (player.alive || roundResult) - drawSpotShadow(player.x, player.y, player.z, player.ducking ? 52 : 64, - shadowInk); - for (const item of balls) - if (item.active) - drawSpotShadow(item.x, item.y, item.z, item.radius * 1.18, shadowInk); - const windInk = windDirection < 0 - ? mixColor([72, 174, 255], [28, 88, 188], visualTheme.light) - : mixColor([255, 92, 132], [184, 35, 62], visualTheme.light); - drawAmbientMotes(windInk); - if (WIND_FLAG && shellMode === "GAME") drawWindFlag(t, windInk); - // The top row is the round's: a clock, and who is watching. The wordmark - // screen carries its own clock, so this one waits for start. - if (shellMode === "GAME") { - const timedRound = roundIsTimed(); - const remainingSeconds = roundResult || !timedRound ? 0 : Math.max(0, - Math.ceil((roundDurationUs - roundElapsedUs) / 1000000)); - const timerText = roundResult - ? roundResult === "TIE" ? "tie!" : "" - : timedRound ? String(remainingSeconds).padStart(2, "0") : "∞"; - const hud = hudSafeRect(); - // Only a television has room to let the endless glyph grow this far. - const timerSize = timedRound ? hudTypeSize - : Math.round(hudTypeSize * (compactLayout() ? 1.65 : 2.6)); - if (timerText === "∞") { - // The endless clock is the whole top row in training, so let it shine: - // it breathes, drifts through the title palette, and drops a colored - // shadow a palette step behind itself. - const glyphSize = Math.round(timerSize * (1 + Math.sin(t * 4.6) * .07)); - const x = viewCenterX() - handleWidth(timerText, glyphSize) / 2; - typeWrite(timerText, x + 7, hud.top + 9, glyphSize, - ...animatedTitleColor(4, t * 2.4)); - typeWrite(timerText, x, hud.top + 2, glyphSize, - ...animatedTitleColor(0, t * 2.4)); - } else { - const timerWidth = handleWidth(timerText, timerSize); - const timerDanger = timedRound && remainingSeconds > 0 && - remainingSeconds <= 10; - const timerShake = timerDanger - ? Math.sin(t * 35) * (11 - remainingSeconds) * .45 : 0; - const timerInk = timerDanger - ? mixColor(titleInk, [235, 38, 58], (11 - remainingSeconds) / 10) - : titleInk; - typeWrite(timerText, viewCenterX() - timerWidth / 2 + timerShake, - hud.top + 2, timerSize, ...timerInk); - } - if (roundViewer) { - const viewerLabel = roundViewerMode || roundViewerStatus; - typeWrite(viewerLabel, hud.right - viewerLabel.length * 18, hud.top + 7, - 24, ...(roundViewerMode === "LIVE" ? [210, 42, 62] : titleInk)); - } - // Wall clock in the top right, tucked left of the round QR and below the - // spectator label so it never lands on either. - const nowMs = run.unixMs || Date.now(); - const clock = hudClockBox(nowMs); - typeWrite(clock.label, clock.left, - hud.top + (roundViewer ? clock.size + 10 : 2), clock.size, ...titleInk); - drawHudStatusTray(clock, titleInk, nowMs); - } - for (const tree of bodyTrees) drawBodyTree(tree, t); - for (const pickup of gunPickups) drawGunPickup(pickup, t); - for (const pickup of grenadePickups) drawGrenadePickup(pickup, t); - const introAge = run.monotonicUs - roundStartedAt; - const showRunnerLabels = Boolean(roundResult) || introAge >= introDurationUs; - const viewDirection = normalize3({ - x: cameraDoll.target.x - cameraDoll.position.x, - y: cameraDoll.target.y - cameraDoll.position.y, - z: cameraDoll.target.z - cameraDoll.position.z, - }); - const renderables = [ - ...bullets.map((item) => ({ - kind: "bullet", item, x: item.x, y: item.y, z: item.z })), - ...grenades.filter((item) => item.alive).map((item) => ({ - kind: "grenade", item, x: item.x, y: item.y, z: item.z })), - ...balls.filter((item) => item.active).map((item) => ({ - kind: "ball", item, x: item.x, y: item.y, z: item.z })), - ...detachedParts.map((item) => ({ kind: "detached", item, - x: (item.x1 + item.x2) / 2, y: (item.y1 + item.y2) / 2, - z: (item.z1 + item.z2) / 2 })), - // Both fighters are always in this list. A fighter is never removed from - // the frame to make a camera move work — the shot is what moves. - ...players.map((item) => ({ - kind: "player", item, x: item.x, y: item.y, z: item.z })), - ]; - const depth = (item) => dot3({ - x: item.x - cameraDoll.position.x, - y: item.y - cameraDoll.position.y, - z: item.z - cameraDoll.position.z, - }, viewDirection); - renderables.sort((a, b) => depth(b) - depth(a)); - for (const renderable of renderables) { - triangleDepth = projectPoint(renderable.x, renderable.y, renderable.z).z; - if (renderable.kind === "bullet") drawBullet(renderable.item); - else if (renderable.kind === "grenade") drawGrenade(renderable.item); - else if (renderable.kind === "ball") drawBall(renderable.item); - else if (renderable.kind === "detached") drawDetachedPart(renderable.item); - else drawRunner(renderable.item, t, showRunnerLabels); - } - // Debug geometry sits above the world but below the -1.42 screen-UI lane. - // It must remain visible through fighter bodies without cutting across the - // title, controls, command notation, names, clock, or other HUD furniture. - triangleDepth = -1.4; - drawDebugHitboxes(players[0], t); - drawDebugHitboxes(players[1], t); - drawBallHitboxes(); - drawSafeZones(); - triangleDepth = -1.42; - drawImpacts(); - const counting = !roundResult && introAge < introDurationUs; - // The matchup card announces two names in the middle of the screen, which - // is exactly where the wordmark sits. On the entry fight the word wins. - if (counting && shellMode === "GAME") - drawFightIntro(introAge / 1000000, titleInk, statusShadow); - // The keys belong wherever a newcomer is looking: under the wordmark on the - // way in, and again while a round counts itself off. Self-play has no - // newcomer — two bots need no tutorial, and neither does a reel of them. - // The legend is the stable key for the score written beneath it. Keep it - // present throughout every player-controlled screen; only unattended bot - // self-play and marketing reels have no learner to serve. - if (!selfPlay) drawControlLegend(titleInk); - const resultUiReady = cinematicAge < 0 || cinematicAge >= 1.1; - if (roundResult && resultUiReady) { - if (INSTANT_REPLAY && instantReplay) { - const frame = Math.min(instantReplay.frames.length, - Math.floor(instantReplay.cursor) + 1); - const replayLabel = "REPLAY " + frame + "/" + instantReplay.frames.length; - typeWrite(replayLabel, viewCenterX() - replayLabel.length * 10, - 820, 30, ...titleInk); - drawCenteredKeycapRun(replayControlKeys(instantReplay.paused), - 940, 19, inputPads[0]?.down || [], titleInk); - } else { - const result = resultCardText(); - const winnerSize = Math.min(92, - Math.max(40, (viewWidth() - 72) / Math.max(1, result.winner.length * .85))); - const winnerWidth = handleWidth(result.winner, winnerSize); - typeWrite(result.winner, viewCenterX() - winnerWidth / 2 + 5, 816, - winnerSize, ...statusShadow); - typeWrite(result.winner, viewCenterX() - winnerWidth / 2, 810, - winnerSize, ...titleInk); - if (result.action) { - const actionSize = Math.min(44, winnerSize * .54); - const actionWidth = handleWidth(result.action, actionSize); - typeWrite(result.action, viewCenterX() - actionWidth / 2 + 3, 900, - actionSize, ...statusShadow); - typeWrite(result.action, viewCenterX() - actionWidth / 2, 896, - actionSize, ...titleInk); - } - if (!roundViewer && INSTANT_REPLAY) { - drawCenteredKeycapRun(replayOfferKeys(), 940, 19, - inputPads[0]?.down || [], titleInk); - } - } - } - // Nameplates and stats wait for the wordmark to lift; the entry frame is - // the word, the keys, and the two fighters, and nothing else. - if (shellMode === "GAME" && ((roundResult && resultUiReady) || - (!roundResult && introAge >= introDurationUs))) { - drawPlayerHandle(players[0], t, 0); - drawPlayerHandle(players[1], t, 1); - drawPlayerStats(players[0], 0, t); - drawPlayerStats(players[1], 1, t); - drawHudInventory(players[0], 0); - drawHudInventory(players[1], 1); - drawCommandStream(players[0], 0); - drawCommandStream(players[1], 1); - } - drawDeathFlash(); - if (shellMode === "MENU") { - const transitionAge = titleTransitionAt !== null - ? (run.monotonicUs - titleTransitionAt) / 1000000 : -1; - drawTitleScreen(t, menuInk, transitionAge); - if (transitionAge >= 0) return; - } - drawSpectatorQr(titleInk); - drawTouchControls(); -} - -function boot() { - try { - gameBoot(); - } catch (error) { - captureClientError("boot", error); - } -} - -function sim() { - if (clientError) { restartAfterClientError(); return; } - try { - gameSim(); - } catch (error) { - captureClientError("sim", error); - } -} - -function paint() { - if (clientError) { - try { drawClientError(); } - catch (_) { drawClientErrorFallback(); } - return; - } - try { - gamePaint(); - } catch (error) { - captureClientError("paint", error); - try { drawClientError(); } - catch (_) { drawClientErrorFallback(); } - } -} - -function act() {} -function leave() { - try { - roundViewerStop?.(); - roundViewerStop = null; - roundViewer = null; - } catch (error) { - captureClientError("leave", error); - } -} diff --git a/xbox/live/mac-test.html b/xbox/live/mac-test.html index 7818530776..a68bba17aa 100644 --- a/xbox/live/mac-test.html +++ b/xbox/live/mac-test.html @@ -2,7 +2,7 @@ - + oskiewar @@ -70,6 +70,10 @@ left: max(14px, env(safe-area-inset-left)); bottom: max(14px, env(safe-area-inset-bottom)); display: flex; flex-direction: row; align-items: center; gap: 10px; } + .touch-enabled #account { + top: max(12px, env(safe-area-inset-top)); + bottom: auto; + } /* The silent auth check answers well after the first paint. In a row that is fine — the handle appears to the left of a button that has not moved — where the old stacked layout made the door jump a second in. */ @@ -117,6 +121,11 @@ document.body.classList.add("title-open"); document.body.classList.toggle("social-preview", new URLSearchParams(location.search).has("social-preview")); + // Direct native/web route for the large destructible training opponent. + // Pal select is intentionally retired; this keeps Spiderdummy reachable + // without restoring a screen that no longer matches the front door. + globalThis.__oskiewarOpponent = + new URLSearchParams(location.search).get("opponent") || ""; let titleOpen = true; const keyMap = new Map([ ["KeyW", [0, "ArrowUp"]], ["KeyS", [0, "ArrowDown"]], @@ -131,6 +140,10 @@ ["KeyK", [1, "A"]], ["KeyL", [1, "B"]], ["Semicolon", [1, "X"]], ["Quote", [1, "Y"]], ]); + globalThis.__oskiewarToggleDebug = () => { + keys[0].add("View"); + setTimeout(() => keys[0].delete("View"), 90); + }; let audioContext = null; const logout = document.querySelector("#logout"); logout.addEventListener("click", () => { @@ -150,6 +163,7 @@ // the only shared ground, so ask it silently — no redirect, no interruption // to a title screen someone may have opened just to play. const accountHandle = document.querySelector("#account-handle"); + const accountProfile = { handle: "", colors: [] }; function loadAuth0() { return new Promise((resolve, reject) => { const script = document.createElement("script"); @@ -187,6 +201,13 @@ if (!handle) return; accountHandle.textContent = "@" + handle; accountHandle.hidden = false; + accountProfile.handle = "@" + String(handle).toUpperCase(); + try { + const colorsResponse = await fetch("https://aesthetic.computer/api/handle-colors?handle=" + + encodeURIComponent(handle), { cache: "no-store" }); + const colors = colorsResponse.ok ? (await colorsResponse.json())?.colors : null; + accountProfile.colors = Array.isArray(colors) ? colors : []; + } catch (_) {} } catch (_) {} } // The poster and the loop are burned headless from this same shell, and a @@ -764,7 +785,9 @@ } function ac() { - return { fighters: [], moodHandle: "", mood: "", source: "web-fallback" }; + return { fighters: accountProfile.handle ? [{ + handle: accountProfile.handle, colors: accountProfile.colors, + }] : [], player: accountProfile, moodHandle: "", mood: "", source: "web-auth" }; } addEventListener("keydown", (event) => { @@ -933,7 +956,8 @@ const updateReady = document.querySelector("#update-ready"); const updateNow = document.querySelector("#update-now"); const autoUpdate = document.querySelector("#auto-update"); - try { autoUpdate.checked = localStorage.getItem("oskiewar-auto-update") === "1"; } + try { autoUpdate.checked = new URLSearchParams(location.search).get("app") === "ios" || + localStorage.getItem("oskiewar-auto-update") === "1"; } catch {} updateNow.addEventListener("click", () => location.reload()); autoUpdate.addEventListener("change", () => { @@ -949,7 +973,7 @@ async function checkForUpdate() { if (!loadedVersion || updateReady.classList.contains("visible")) return; try { - const response = await fetch(`/hello.js?update-check=${Date.now()}`, + const response = await fetch(`/oskiewar.js?update-check=${Date.now()}`, { method: "HEAD", cache: "no-store" }); const availableVersion = responseVersion(response); if (response.ok && availableVersion && availableVersion !== loadedVersion) { @@ -967,10 +991,10 @@ async function start() { let source = globalThis.__fightPieceSource; if (!source) { - const response = await fetch("/hello.js", { cache: "no-store" }); + const response = await fetch("/oskiewar.js", { cache: "no-store" }); loadedVersion = responseVersion(response); source = await response.text(); - setInterval(checkForUpdate, 15000); + setInterval(checkForUpdate, 2000); } const piece = new Function( "runtime", "gamepad", "controllers", "capabilities", "telemetry", diff --git a/xbox/live/marketing/render.mjs b/xbox/live/marketing/render.mjs index d3806153c3..b69ccc1b28 100644 --- a/xbox/live/marketing/render.mjs +++ b/xbox/live/marketing/render.mjs @@ -1,6 +1,6 @@ // Stage 2 — burn a source spec into a 1080x1920 mp4 with the game's own audio. // -// The renderer is the real game in a real browser: `hello.js` under +// The renderer is the real game in a real browser: `oskiewar.js` under // `mac-test.html`, painted at 60 Hz by the same frame driver a player gets. // Two halves, muxed by ffmpeg, both borrowed from `marketing/av-reels`: // @@ -14,7 +14,7 @@ // the headless rAF throttle that leaves `canvas.captureStream()` // empty. // -// Determinism: `hello.js` calls `Math.random` exactly once, to seed the match +// Determinism: `oskiewar.js` calls `Math.random` exactly once, to seed the match // name — everything downstream (ball kind, round names, the whole physics // sim) falls out of that. Replacing `Math.random` with a seeded generator // before boot therefore makes an entire match reproducible from a string. @@ -26,7 +26,7 @@ import { serveShell, repo } from "./shell.mjs"; const wait = (ms) => new Promise((done) => setTimeout(done, ms)); -// Both read off `hello.js`. A round announces its end by POSTing its replay, +// Both read off `oskiewar.js`. A round announces its end by POSTing its replay, // and that POST lands at the START of the result card, not the end of it — // `roundResultUs` of card still has to play before the next round begins. // Getting this backwards is what put ~3 s of the previous round's card on the @@ -74,7 +74,7 @@ async function loadPuppeteer() { } // A fight worth watching, spelled in the keys a person actually holds. -// `hello.js` reads pad one from W/A/S/D + F(kick) G(shield) H(punch) V(item), +// `oskiewar.js` reads pad one from W/A/S/D + F(kick) G(shield) H(punch) V(item), // so the score is legible against the on-screen legend. const combatKeys = { kick: "KeyF", punch: "KeyH", shield: "KeyG", item: "KeyV", jump: "KeyW", left: "KeyA", right: "KeyD" }; @@ -302,7 +302,7 @@ export async function renderReel(spec, { log = console.log } = {}) { } })() : null; - // A match ends when the game posts its demo. `hello.js` carries a + // A match ends when the game posts its demo. `oskiewar.js` carries a // best-of-five constant, but nothing accumulates round wins toward it — // self-play calls `startSelfPlay` again at every round end, and normal // play returns to the title, so every recorded round is its own match. diff --git a/xbox/live/marketing/segments.mjs b/xbox/live/marketing/segments.mjs index e0e71fab10..b2bf11d6ee 100644 --- a/xbox/live/marketing/segments.mjs +++ b/xbox/live/marketing/segments.mjs @@ -91,7 +91,7 @@ export const segments = { export const segmentKeys = Object.keys(segments); -// One accent per market, drawn from the fighters' own palette in `hello.js` +// One accent per market, drawn from the fighters' own palette in `oskiewar.js` // so a reel never introduces a colour the game does not already own. export const accents = { fgc: "#cd3048", gamedev: "#6fe8d2", homebrew: "#3076cd", diff --git a/xbox/live/marketing/shell.mjs b/xbox/live/marketing/shell.mjs index 426690b926..603ad3c06d 100644 --- a/xbox/live/marketing/shell.mjs +++ b/xbox/live/marketing/shell.mjs @@ -18,9 +18,9 @@ export const live = resolve(fileURLToPath(new URL("..", import.meta.url))); export const repo = resolve(live, "../.."); // Hashing these tells a burner whether its output still matches the game. -export const shellSources = ["hello.js", "mac-test.html", "frame-driver.mjs"]; +export const shellSources = ["oskiewar.js", "mac-test.html", "frame-driver.mjs"]; -const fromLive = ["hello.js", "oskiewar-sfx.mjs", "oskiewar-midi.mjs", +const fromLive = ["oskiewar.js", "oskiewar-sfx.mjs", "oskiewar-midi.mjs", "frame-driver.mjs", "round-room.mjs"]; const fromPublic = ["aesthetic.computer/dep/@akamfoad/qr/qr.mjs", "aesthetic.computer/lib/product-analytics.mjs", diff --git a/xbox/live/oskiewar-frame-design.md b/xbox/live/oskiewar-frame-design.md index d34971bee1..627e71d30b 100644 --- a/xbox/live/oskiewar-frame-design.md +++ b/xbox/live/oskiewar-frame-design.md @@ -1,6 +1,6 @@ # oskiewar pose and frame system -Target: original geometry-only fighters in `xbox/live/hello.js`, simulated at 60 Hz. Street Fighter references are timing and readability research only. Do not import, trace, ship, or derive coordinates from copyrighted sprites, captures, models, or hitbox images. +Target: original geometry-only fighters in `xbox/live/oskiewar.js`, simulated at 60 Hz. Street Fighter references are timing and readability research only. Do not import, trace, ship, or derive coordinates from copyrighted sprites, captures, models, or hitbox images. ## Current behavior to replace diff --git a/xbox/live/oskiewar-midi.mjs b/xbox/live/oskiewar-midi.mjs index 0b23114432..20b3bc5f22 100644 --- a/xbox/live/oskiewar-midi.mjs +++ b/xbox/live/oskiewar-midi.mjs @@ -1,7 +1,7 @@ // oskiewar Web MIDI bridge. // // Every gameSignal is already broadcast as an `oskiewar:signal` CustomEvent so -// `oskiewar-sfx.mjs` can synthesize it (`hello.js` `emitSignal`). This module is +// `oskiewar-sfx.mjs` can synthesize it (`oskiewar.js` `emitSignal`). This module is // a second subscriber on the same bus that sends the stream out as MIDI, so a // DAW on the same machine can be the instrument instead of the procedural bank. // It never touches the sfx module; muting one is the host's decision. diff --git a/xbox/live/oskiewar-sfx.mjs b/xbox/live/oskiewar-sfx.mjs index 796e2eef17..3a2f767c7b 100644 --- a/xbox/live/oskiewar-sfx.mjs +++ b/xbox/live/oskiewar-sfx.mjs @@ -21,7 +21,7 @@ const POP_TONES = Object.freeze({ hellsine: [185, 220, 277.18, 329.63], }); -// Complete literal/dynamic gameSignal vocabulary audited from hello.js. +// Complete literal/dynamic gameSignal vocabulary audited from oskiewar.js. export const OSKIEWAR_SIGNAL_EVENTS = Object.freeze([ "hello", "select", "fighters", "wind", "ballserve", "countdown", "tie", "roundwin", "matchwin", diff --git a/xbox/live/oskiewar.js b/xbox/live/oskiewar.js index 90abc2b01a..7e6081e08d 100644 --- a/xbox/live/oskiewar.js +++ b/xbox/live/oskiewar.js @@ -634,6 +634,9 @@ const ballKinds = [ bounce: .76, drag: .989, windFactor: .34, gravityFactor: 1 }, { type: "beach", spawnOwner: -1, radius: 46, mass: .34, hitScale: 1.25, bounce: .82, drag: .998, windFactor: 1.35, gravityFactor: .62 }, + { type: "skateboard", spawnOwner: -1, radius: 52, mass: 1.4, + hitScale: .82, bounce: .34, drag: .982, windFactor: .12, + gravityFactor: 1 }, ]; let matchBallType = ballKinds[0].type; // A match inflates exactly one ball and keeps it for every round. @@ -691,6 +694,15 @@ let hudLeftPad = 0; // Hashing the already-created round name keeps the split stable for a visit // without spending a second Math.random call (reel seeding relies on one). let titleAttractMode = "still"; +let trainingOpponent = ""; +function trainingOpponentKind() { + const requested = String(globalThis.__oskiewarOpponent || "").toLowerCase(); + if (requested === "dummy" || requested === "spiderdummy") return requested; + if (!trainingOpponent) trainingOpponent = + hashUnit("training-opponent:" + String(runtime().unixMs || Date.now())) < .5 + ? "dummy" : "spiderdummy"; + return trainingOpponent; +} const selectionReady = [false, false]; const selectionPrevious = [[], []]; let selectionStep = 0; @@ -1655,7 +1667,7 @@ function startFightAgainst(kind, now) { // floats over it. The intro countdown is spent before the first frame so // somebody arriving from a QR code is moving, not watching a number. function beginTraining(now) { - startFightAgainst("dummy", now); + startFightAgainst(trainingOpponentKind(), now); shellMode = "MENU"; gameplayStarted = false; roundStartedAt = now - introDurationUs; @@ -6497,6 +6509,39 @@ function drawTerrainSurface(left, right, near, far, color) { } } +function drawRoomSurfaces(left, right, top, bottom, color) { + const columns = 6; + const rows = 2; + for (let row = 0; row < rows; row++) { + for (let column = 0; column < columns; column++) { + const x1 = lerp(left, right, column / columns); + const x2 = lerp(left, right, (column + 1) / columns); + const floor1 = terrainFloorAt(x1); + const floor2 = terrainFloorAt(x2); + const y1Left = lerp(top, floor1, row / rows); + const y1Right = lerp(top, floor2, row / rows); + const y2Left = lerp(top, floor1, (row + 1) / rows); + const y2Right = lerp(top, floor2, (row + 1) / rows); + const grain = .5 + .5 * Math.sin(column * 17.7 + row * 43.1 + + terrainPhase * 2.4); + const shade = mixColor(color, [112, 126, 116], .08 + grain * .12); + worldQuad({ x: x1, y: y1Left, z: worldFar }, + { x: x2, y: y1Right, z: worldFar }, + { x: x2, y: y2Right, z: worldFar }, + { x: x1, y: y2Left, z: worldFar }, shade); + } + } + const edge = mixColor(color, [64, 78, 72], .24); + worldQuad({ x: left, y: ceilingY, z: worldNear }, + { x: left, y: ceilingY, z: worldFar }, + { x: right, y: ceilingY, z: worldFar }, + { x: right, y: ceilingY, z: worldNear }, edge); + worldLine(worldLeft, top, worldFar - 4, worldLeft, bottom, worldFar - 4, + 9, edge); + worldLine(worldRight, top, worldFar - 4, worldRight, bottom, worldFar - 4, + 9, edge); +} + function drawTerrainGrass(left, right, color) { // Twenty deterministic tufts cost forty native lines total. Keeping them on // the line layer avoids turning background dressing into hundreds of faces. @@ -6599,6 +6644,7 @@ function drawBall(ball) { point.y - radius > viewHeight) return; const soccer = ball.type === "soccer"; const beach = ball.type === "beach"; + const skateboard = ball.type === "skateboard"; const polygon = (x, y, polygonRadius, sides, rotation, color) => { for (let side = 0; side < sides; side++) { const a = rotation + side * Math.PI * 2 / sides; @@ -6610,6 +6656,34 @@ function drawBall(ball) { y + Math.sin(b) * polygonRadius, ...color); } }; + if (skateboard) { + const alongX = Math.cos(ball.rotation) * radius; + const alongY = Math.sin(ball.rotation) * radius; + const normalX = -Math.sin(ball.rotation); + const normalY = Math.cos(ball.rotation); + const deck = [236, 76, 118]; + const edge = [34, 25, 39]; + const truck = [174, 184, 202]; + filledCapsule(point.x - alongX * .86, point.y - alongY * .86, + point.x + alongX * .86, point.y + alongY * .86, + Math.max(8, radius * .22), edge); + filledCapsule(point.x - alongX * .82, point.y - alongY * .82, + point.x + alongX * .82, point.y + alongY * .82, + Math.max(5, radius * .14), deck); + for (const direction of [-1, 1]) { + const truckX = point.x + alongX * direction * .56; + const truckY = point.y + alongY * direction * .56; + filledCapsule(truckX - normalX * radius * .22, + truckY - normalY * radius * .22, + truckX + normalX * radius * .22, + truckY + normalY * radius * .22, Math.max(2, radius * .055), truck); + for (const side of [-1, 1]) + filledDisc(truckX + normalX * radius * side * .26, + truckY + normalY * radius * side * .26, + Math.max(3, radius * .09), [22, 25, 34]); + } + return; + } if (beach) { const panels = [ [244, 66, 96], [255, 146, 52], [255, 226, 66], [72, 210, 126], @@ -7464,6 +7538,7 @@ function gamePaint() { cameraDoll.prepare(); const { left: spanLeft, right: spanRight, top: spanTop, bottom: spanBottom } = terrainSpan(); + drawRoomSurfaces(spanLeft, spanRight, spanTop, spanBottom, arena); drawTerrainSurface(spanLeft, spanRight, worldNear, worldFar, ground); drawTerrainGrass(spanLeft, spanRight, ground); const platformNear = -520; diff --git a/xbox/live/render-social-preview.mjs b/xbox/live/render-social-preview.mjs index dac48d641c..989ab28060 100644 --- a/xbox/live/render-social-preview.mjs +++ b/xbox/live/render-social-preview.mjs @@ -18,7 +18,7 @@ const posterSize = { width: 1200, height: 630 }; const videoSize = { width: 720, height: 1280 }; const captureSeconds = 2; const loopSeconds = captureSeconds * 2; -const sources = ["hello.js", "mac-test.html", "frame-driver.mjs"]; +const sources = ["oskiewar.js", "mac-test.html", "frame-driver.mjs"]; const sourceHash = createHash("sha256"); for (const name of sources) sourceHash.update(await readFile(join(live, name))); sourceHash.update(await readFile(new URL(import.meta.url))); @@ -42,7 +42,7 @@ function fileFor(pathname) { return join(live, "mac-test.html"); // Every module the shell imports has to be listed, or the page dies on a 404 // and the capture comes out empty rather than loudly wrong. - if (["/hello.js", "/oskiewar-sfx.mjs", "/oskiewar-midi.mjs", "/frame-driver.mjs", + if (["/oskiewar.js", "/oskiewar-sfx.mjs", "/oskiewar-midi.mjs", "/frame-driver.mjs", "/round-room.mjs"].includes(pathname)) return join(live, pathname.slice(1)); if (pathname === "/aesthetic.computer/dep/@akamfoad/qr/qr.mjs") return join(repo, "system/public/aesthetic.computer/dep/@akamfoad/qr/qr.mjs"); diff --git a/xbox/live/replay/README.md b/xbox/live/replay/README.md new file mode 100644 index 0000000000..453307834c --- /dev/null +++ b/xbox/live/replay/README.md @@ -0,0 +1,42 @@ +# Oskiewar replay compatibility + +A demo is an input recording for one immutable simulation, not a recording for +“the latest Oskiewar.” Old demos remain accurate only while all three identities +stay explicit: + +- `version` selects the envelope decoder and migration chain. +- `simulation` selects an archived physics adapter. +- `build` records which integer game build made the demo; it is provenance, not + permission to substitute that build's physics with current physics. + +`compat.mjs` is the append-only compatibility boundary. New physics receives a +new simulation identifier and adapter entry. Never change an existing contract's +tick duration, button bits, checkpoint width, or behavior. Never automatically +map an unknown simulation to the newest one: an honest “adapter unavailable” is +better than a plausible but inaccurate replay. + +Playback first migrates the old envelope, validates its immutable simulation, +then expands command state changes on a fixed 60 Hz tick clock. Paint cadence, +refresh rate, frame drops, and wall time do not participate. Fixture digests pin +both the migrated payload and expanded input timeline so decoder drift fails in +focused tests. + +`fixtures/manifest.json` is itself versioned and records the producing build, +minimum player build, canonical replay digest, and expanded-timeline digest for +each preserved recording. The minimum build says where the decoder first +shipped; it never authorizes a different simulation adapter. + +To add a format or simulation: + +1. Preserve a real recording as a new fixture; do not rewrite old fixtures. +2. Add a one-way envelope migration without deleting the earlier decoder. +3. Add the archived simulation contract/adapter under its original identifier. +4. Assert the fixture's canonical and timeline digests plus a late-player-build + playback plan. +5. Run `node --test xbox/live/tests/replay-compat.test.mjs`. + +The compatibility layer intentionally does not claim that current physics can +re-simulate `oskiewar-physics-1`. The runtime must ultimately dispatch the plan +to a preserved adapter with that exact ID. Until that adapter is extracted from +the live game, stored v1 demos may use their checkpoint viewer, while command +re-simulation should fail closed rather than drift. diff --git a/xbox/live/replay/compat.mjs b/xbox/live/replay/compat.mjs new file mode 100644 index 0000000000..96c036bd3d --- /dev/null +++ b/xbox/live/replay/compat.mjs @@ -0,0 +1,167 @@ +// Oskiewar replay compatibility lives outside the moving game implementation. +// A recording selects an immutable simulation contract; it is never silently +// reinterpreted using whatever physics happens to ship in the current build. + +import { createHash } from "node:crypto"; + +export const REPLAY_FORMAT = "ac.oskiedemo"; +export const CURRENT_REPLAY_VERSION = 1; +export const FIXTURE_MANIFEST_FORMAT = "ac.oskiewar.replay-fixtures"; + +// Append-only. Existing identifiers and their input layouts must not change. +// When physics changes, add another entry and retain the old playback adapter. +export const simulationContracts = Object.freeze({ + "oskiewar-physics-1": Object.freeze({ + id: "oskiewar-physics-1", + tickRate: 60, + tickUs: 16667, + inputEncoding: "oskiewar-buttons-v1", + buttons: Object.freeze([ + "left", "right", "up", "down", "kick", "punch", "shield", "item", + ]), + checkpointEncoding: "oskiewar-checkpoint-26-v1", + checkpointWidth: 26, + firstBuild: 1, + }), +}); + +const integer = (value, min = 0, max = Number.MAX_SAFE_INTEGER) => + Number.isInteger(value) && value >= min && value <= max; + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function migrateLegacyZero(source) { + const demo = clone(source); + demo.format = REPLAY_FORMAT; + demo.version = 1; + demo.game = demo.game || "oskiewar"; + demo.simulation = demo.simulation || "oskiewar-physics-1"; + demo.tickRate = demo.tickRate || 60; + demo.commands = demo.commands || demo.inputs || []; + delete demo.inputs; + demo.events ||= []; + demo.checkpoints ||= []; + demo.rounds ||= []; + demo.roundIds ||= demo.roundId ? [demo.roundId] : []; + demo.roundIndex ??= 0; + demo.durationTicks ??= demo.ticks ?? 0; + delete demo.ticks; + return demo; +} + +export function migrateReplay(source) { + if (!source || typeof source !== "object" || Array.isArray(source)) + throw new TypeError("Replay must be an object"); + if (source.format !== undefined && source.format !== REPLAY_FORMAT) + throw new Error(`Unsupported replay format: ${source.format}`); + if (source.version === undefined || source.version === 0) + return migrateLegacyZero(source); + if (source.version === 1) return clone(source); + throw new Error(`No decoder for replay version ${source.version}`); +} + +export function validateCompatibleReplay(source) { + let demo; + try { demo = migrateReplay(source); } + catch (error) { return { ok: false, error: error.message }; } + if (demo.format !== REPLAY_FORMAT || demo.version !== CURRENT_REPLAY_VERSION || + demo.game !== "oskiewar") return { ok: false, error: "Unsupported replay envelope" }; + const simulation = simulationContracts[demo.simulation]; + if (!simulation) + return { ok: false, error: `Archived simulation unavailable: ${demo.simulation}` }; + if (demo.tickRate !== simulation.tickRate) + return { ok: false, error: "Tick rate does not match simulation contract" }; + if (!integer(demo.durationTicks, 0, 216000)) + return { ok: false, error: "Invalid replay duration" }; + if (!Array.isArray(demo.commands) || demo.commands.length > 50000) + return { ok: false, error: "Invalid command timeline" }; + let priorTick = -1; + for (const row of demo.commands) { + if (!Array.isArray(row) || row.length !== 3 || + !integer(row[0], 0, demo.durationTicks) || !integer(row[1], 0, 1) || + !integer(row[2], 0, 255)) + return { ok: false, error: "Invalid command row" }; + if (row[0] < priorTick) + return { ok: false, error: "Command timeline is not monotonic" }; + priorTick = row[0]; + } + if (!Array.isArray(demo.checkpoints) || demo.checkpoints.some((row) => + !Array.isArray(row) || row.length !== simulation.checkpointWidth || + row.some((value) => !Number.isFinite(value)))) + return { ok: false, error: "Invalid checkpoint stream" }; + return { ok: true, demo, simulation }; +} + +// Expands state changes into the exact two-pad mask visible at each sim tick. +// This is deliberately independent of rendering cadence and wall-clock time. +export function inputTimeline(source) { + const result = validateCompatibleReplay(source); + if (!result.ok) throw new Error(result.error); + const { demo } = result; + const states = [0, 0]; + const byTick = new Map(); + for (const [tick, pad, mask] of demo.commands) { + const rows = byTick.get(tick) || []; + rows.push([pad, mask]); + byTick.set(tick, rows); + } + const timeline = []; + for (let tick = 0; tick <= demo.durationTicks; tick++) { + for (const [pad, mask] of byTick.get(tick) || []) states[pad] = mask; + timeline.push([tick, states[0], states[1]]); + } + return timeline; +} + +function stable(value) { + if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`; + if (value && typeof value === "object") return `{${Object.keys(value).sort() + .filter((key) => value[key] !== undefined) + .map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(",")}}`; + return JSON.stringify(value); +} + +export function replayDigest(source) { + const result = validateCompatibleReplay(source); + if (!result.ok) throw new Error(result.error); + return createHash("sha256").update(stable(result.demo)).digest("hex"); +} + +export function timelineDigest(source) { + return createHash("sha256").update(stable(inputTimeline(source))).digest("hex"); +} + +export function playbackPlan(source, playerBuild) { + const result = validateCompatibleReplay(source); + if (!result.ok) throw new Error(result.error); + if (!integer(playerBuild, 1)) throw new Error("Player build must be an integer"); + return Object.freeze({ + demo: result.demo, + simulation: result.simulation.id, + decoder: `${REPLAY_FORMAT}-v${result.demo.version}`, + clock: Object.freeze({ tickRate: result.simulation.tickRate, + tickUs: result.simulation.tickUs }), + recordingBuild: integer(result.demo.build, 1) ? result.demo.build : null, + playerBuild, + timeline: inputTimeline(result.demo), + }); +} + +export function validateFixtureManifest(manifest) { + if (!manifest || manifest.format !== FIXTURE_MANIFEST_FORMAT || + manifest.version !== 1 || !Array.isArray(manifest.fixtures)) + return { ok: false, error: "Unsupported fixture manifest" }; + const names = new Set(); + for (const item of manifest.fixtures) { + if (!item || typeof item.name !== "string" || names.has(item.name) || + typeof item.file !== "string" || !/^[-a-z0-9]+\.json$/.test(item.file) || + !integer(item.recordingBuild, 1) || !integer(item.minimumPlayerBuild, 1) || + !/^[a-f0-9]{64}$/.test(item.replayDigest || "") || + !/^[a-f0-9]{64}$/.test(item.timelineDigest || "")) + return { ok: false, error: `Invalid fixture: ${item?.name || "unnamed"}` }; + names.add(item.name); + } + return { ok: true, manifest: clone(manifest) }; +} diff --git a/xbox/live/replay/fixtures/legacy-v0.json b/xbox/live/replay/fixtures/legacy-v0.json new file mode 100644 index 0000000000..37e25aa447 --- /dev/null +++ b/xbox/live/replay/fixtures/legacy-v0.json @@ -0,0 +1,16 @@ +{ + "game": "oskiewar", + "simulation": "oskiewar-physics-1", + "tickRate": 60, + "build": 37, + "matchId": "ow-demo-old", + "matchName": "demo-old", + "roundId": "ow-demo-old", + "roundName": "demo-old", + "durationTicks": 8, + "fighters": ["@OLD", "DUMMY"], + "inputs": [[0, 0, 2], [2, 1, 16], [4, 0, 0], [7, 1, 0]], + "events": [], + "checkpoints": [], + "rounds": [] +} diff --git a/xbox/live/replay/fixtures/manifest.json b/xbox/live/replay/fixtures/manifest.json new file mode 100644 index 0000000000..9477c5dcea --- /dev/null +++ b/xbox/live/replay/fixtures/manifest.json @@ -0,0 +1,22 @@ +{ + "format": "ac.oskiewar.replay-fixtures", + "version": 1, + "fixtures": [ + { + "name": "legacy-build-37", + "file": "legacy-v0.json", + "recordingBuild": 37, + "minimumPlayerBuild": 500, + "replayDigest": "6889787b66b1708b8915be809e0afcb7728e34dd199a8b6a59bc2763d9a02078", + "timelineDigest": "a6d46e00073439d314fce7d278a2827078a9fcedc6c1eb3eb22c01e34aea330b" + }, + { + "name": "version-one-build-500", + "file": "v1.json", + "recordingBuild": 500, + "minimumPlayerBuild": 500, + "replayDigest": "ca36d27675af75123121de9b3a72af1af2a7bb21e6ade38798ce2ce470022437", + "timelineDigest": "865d88c02a15a9fb9328f674e2811cee08f7971edb724fbb16df2ad2760b98c5" + } + ] +} diff --git a/xbox/live/replay/fixtures/v1.json b/xbox/live/replay/fixtures/v1.json new file mode 100644 index 0000000000..c71d550b2e --- /dev/null +++ b/xbox/live/replay/fixtures/v1.json @@ -0,0 +1,20 @@ +{ + "format": "ac.oskiedemo", + "version": 1, + "game": "oskiewar", + "simulation": "oskiewar-physics-1", + "tickRate": 60, + "build": 500, + "matchId": "ow-demo-new", + "matchName": "demo-new", + "roundId": "ow-demo-new", + "roundName": "demo-new", + "roundIds": ["ow-demo-new"], + "roundIndex": 0, + "durationTicks": 5, + "fighters": ["@NEW", "DUMMY"], + "commands": [[0, 0, 1], [1, 0, 17], [3, 0, 0]], + "events": [], + "checkpoints": [], + "rounds": [] +} diff --git a/xbox/live/scene3d-webgl.mjs b/xbox/live/scene3d-webgl.mjs new file mode 100644 index 0000000000..4526384281 --- /dev/null +++ b/xbox/live/scene3d-webgl.mjs @@ -0,0 +1,109 @@ +import { OskiewarScene3D } from "./scene3d.mjs"; + +const vertexSource = `#version 300 es +precision highp float; +layout(location = 0) in vec3 position; +layout(location = 1) in vec3 color; +out vec3 ink; +void main() { + gl_Position = vec4(position, 1.0); + ink = color; +}`; + +const fragmentSource = `#version 300 es +precision highp float; +in vec3 ink; +out vec4 pixel; +void main() { pixel = vec4(ink, 1.0); }`; + +function compile(gl, type, source) { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + const message = gl.getShaderInfoLog(shader) || "shader compilation failed"; + gl.deleteShader(shader); + throw new Error(message); + } + return shader; +} + +function program(gl) { + const vertex = compile(gl, gl.VERTEX_SHADER, vertexSource); + const fragment = compile(gl, gl.FRAGMENT_SHADER, fragmentSource); + const result = gl.createProgram(); + gl.attachShader(result, vertex); + gl.attachShader(result, fragment); + gl.linkProgram(result); + gl.deleteShader(vertex); + gl.deleteShader(fragment); + if (!gl.getProgramParameter(result, gl.LINK_STATUS)) { + const message = gl.getProgramInfoLog(result) || "scene program link failed"; + gl.deleteProgram(result); + throw new Error(message); + } + return result; +} + +export class WebGLOskiewarScene3D { + constructor(canvas, options = {}) { + const gl = canvas.getContext("webgl2", { + alpha: true, antialias: true, depth: true, stencil: false, + premultipliedAlpha: false, preserveDrawingBuffer: false, + }); + if (!gl) throw new Error("WebGL 2 is unavailable"); + this.canvas = canvas; + this.gl = gl; + this.scene = new OskiewarScene3D(options); + this.program = program(gl); + this.array = gl.createVertexArray(); + this.buffer = gl.createBuffer(); + gl.bindVertexArray(this.array); + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferData(gl.ARRAY_BUFFER, this.scene.vertices.byteLength, + gl.DYNAMIC_DRAW); + const stride = 6 * Float32Array.BYTES_PER_ELEMENT; + gl.enableVertexAttribArray(0); + gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, 0); + gl.enableVertexAttribArray(1); + gl.vertexAttribPointer(1, 3, gl.FLOAT, false, stride, + 3 * Float32Array.BYTES_PER_ELEMENT); + gl.bindVertexArray(null); + gl.enable(gl.DEPTH_TEST); + gl.depthFunc(gl.LEQUAL); + gl.disable(gl.CULL_FACE); + } + + resize(pixelWidth, pixelHeight) { + if (this.canvas.width !== pixelWidth) this.canvas.width = pixelWidth; + if (this.canvas.height !== pixelHeight) this.canvas.height = pixelHeight; + } + + beginFrame() { this.scene.beginFrame(); } + + triangle(...values) { return this.scene.triangle(...values); } + + present({ clear = [0, 0, 0, 0] } = {}) { + const gl = this.gl; + gl.viewport(0, 0, this.canvas.width, this.canvas.height); + gl.clearColor(...clear); + gl.clearDepth(1); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + if (!this.scene.triangleCount) return; + gl.useProgram(this.program); + gl.bindVertexArray(this.array); + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.scene.frameVertices()); + gl.drawArrays(gl.TRIANGLES, 0, this.scene.triangleCount * 3); + gl.bindVertexArray(null); + } + + destroy() { + const gl = this.gl; + gl.deleteBuffer(this.buffer); + gl.deleteVertexArray(this.array); + gl.deleteProgram(this.program); + } +} + +export default WebGLOskiewarScene3D; diff --git a/xbox/live/scene3d.mjs b/xbox/live/scene3d.mjs new file mode 100644 index 0000000000..1023f95aca --- /dev/null +++ b/xbox/live/scene3d.mjs @@ -0,0 +1,53 @@ +// Oskiewar scene contract shared by browser and native GPU hosts. +// Geometry arrives in logical screen space after the canonical JS camera has +// projected and clipped it. Z remains authoritative and must reach a depth +// buffer; hosts must never discard it or substitute painter ordering. + +export const OSKIEWAR_SCENE_VERSION = 1; +export const OSKIEWAR_MAX_TRIANGLES = 8192; +export const OSKIEWAR_VERTEX_FLOATS = 6; // clip x/y/z + linear r/g/b +export const OSKIEWAR_TRIANGLE_FLOATS = OSKIEWAR_VERTEX_FLOATS * 3; + +export function clipDepth(z) { + if (!Number.isFinite(z)) throw new RangeError("scene depth must be finite"); + // This is the mapping already shipped by the Xbox D3D renderer. Keeping it + // here makes WebGL/Metal/D3D compare the same normalized depth values. + return Math.max(0, Math.min(1, (z + 1.5) / 3)); +} +export function clipPoint(x, y, z, width, height) { + if (![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0) + throw new RangeError("invalid scene viewport or vertex"); + return [x / (width / 2) - 1, 1 - y / (height / 2), clipDepth(z)]; +} + +export class OskiewarScene3D { + constructor({ maxTriangles = OSKIEWAR_MAX_TRIANGLES } = {}) { + this.maxTriangles = maxTriangles; + this.vertices = new Float32Array(maxTriangles * OSKIEWAR_TRIANGLE_FLOATS); + this.triangleCount = 0; + } + + beginFrame() { this.triangleCount = 0; } + + triangle(x1, y1, z1, x2, y2, z2, x3, y3, z3, + r = 255, g = 255, b = 255, width = 1920, height = 1080) { + if (this.triangleCount >= this.maxTriangles) return false; + const points = [clipPoint(x1, y1, z1, width, height), + clipPoint(x2, y2, z2, width, height), + clipPoint(x3, y3, z3, width, height)]; + const color = [r, g, b].map((channel) => + Math.max(0, Math.min(255, Number(channel) || 0)) / 255); + let at = this.triangleCount * OSKIEWAR_TRIANGLE_FLOATS; + for (const point of points) { + this.vertices.set([...point, ...color], at); + at += OSKIEWAR_VERTEX_FLOATS; + } + this.triangleCount++; + return true; + } + + frameVertices() { + return this.vertices.subarray(0, + this.triangleCount * OSKIEWAR_TRIANGLE_FLOATS); + } +} diff --git a/xbox/live/snapshot.mjs b/xbox/live/snapshot.mjs index 0e5d2df051..9585555509 100644 --- a/xbox/live/snapshot.mjs +++ b/xbox/live/snapshot.mjs @@ -70,7 +70,7 @@ const commands = []; let background = [8, 10, 20]; let clock = 0; -const source = await readFile(join(HERE, "hello.js"), "utf8"); +const source = await readFile(join(HERE, "oskiewar.js"), "utf8"); const noOp = () => {}; const pads = [0, 1].map(() => ({ connected: true, down: [], leftX: 0, leftY: 0 })); diff --git a/xbox/live/social/manifest.json b/xbox/live/social/manifest.json index 9ddb0d3abd..dfb727b46c 100644 --- a/xbox/live/social/manifest.json +++ b/xbox/live/social/manifest.json @@ -1,7 +1,7 @@ { "format": "ac.oskiewar.social-preview", "version": 2, - "build": "3be9be64d3a57a12", + "build": "3dba1810ea9ff641", "theme": "light", "imageWidth": 1200, "imageHeight": 630, diff --git a/xbox/live/social/oskiewar-title.jpg b/xbox/live/social/oskiewar-title.jpg index 5973761baf00dadd6d05f5bf8d7d7bf390929578..a003e7a453630b6954098ca605f5dd9c7d0757c4 100644 GIT binary patch literal 52081 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 literal 51696 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 diff --git a/xbox/live/tests/blackbox-rounds.mjs b/xbox/live/tests/blackbox-rounds.mjs index 89c3e73ff4..202b26e62a 100644 --- a/xbox/live/tests/blackbox-rounds.mjs +++ b/xbox/live/tests/blackbox-rounds.mjs @@ -20,7 +20,7 @@ const mime = new Map([ ]); function fileFor(pathname) { - if (pathname === "/hello.js") return join(here, "hello.js"); + if (pathname === "/oskiewar.js") return join(here, "oskiewar.js"); if (pathname === "/oskiewar-sfx.mjs") return join(here, "oskiewar-sfx.mjs"); if (pathname === "/frame-driver.mjs") return join(here, "frame-driver.mjs"); if (pathname === "/round-room.mjs") return join(here, "round-room.mjs"); diff --git a/xbox/live/tests/oskiewar-midi.test.mjs b/xbox/live/tests/oskiewar-midi.test.mjs index 0263c11a84..1c176eb5a7 100644 --- a/xbox/live/tests/oskiewar-midi.test.mjs +++ b/xbox/live/tests/oskiewar-midi.test.mjs @@ -226,7 +226,7 @@ test("the lamp is drawn by the game, not floated over it", async () => { "capabilities carries the pulse rather than adding a sandbox function"); assert.match(webShell, /onSignal: \(\) => \{ globalThis\.__oskiewarMidiPulse = Date\.now\(\); \}/); - const game = await readFile(new URL("../hello.js", import.meta.url), "utf8"); + const game = await readFile(new URL("../oskiewar.js", import.meta.url), "utf8"); assert.match(game, /function hudStatusTray\(/, "the status lane must exist"); assert.match(game, /function drawStatusPiano\(/); assert.match(game, /const statusCell = \d+/, "icons share one cell size"); diff --git a/xbox/live/tests/oskiewar-sfx.test.mjs b/xbox/live/tests/oskiewar-sfx.test.mjs index c624fd5be7..166866b45d 100644 --- a/xbox/live/tests/oskiewar-sfx.test.mjs +++ b/xbox/live/tests/oskiewar-sfx.test.mjs @@ -69,7 +69,7 @@ class FakeAudioContext { } } -test("signal vocabulary covers every current hello.js game event", () => { +test("signal vocabulary covers every current oskiewar.js game event", () => { const required = [ "hello", "select", "fighters", "wind", "ballserve", "countdown", "tie", "roundwin", "matchwin", "move", "jump", "ultrajump", diff --git a/xbox/live/tests/hello.test.mjs b/xbox/live/tests/oskiewar.test.mjs similarity index 94% rename from xbox/live/tests/hello.test.mjs rename to xbox/live/tests/oskiewar.test.mjs index d9c8b106e3..7749547646 100644 --- a/xbox/live/tests/hello.test.mjs +++ b/xbox/live/tests/oskiewar.test.mjs @@ -7,7 +7,7 @@ import { qrcode } from import { decodeDump, dumpRows } from "../../../system/netlify/functions/oskiewar-dump.mjs"; -const source = await readFile(new URL("../hello.js", import.meta.url), "utf8"); +const source = await readFile(new URL("../oskiewar.js", import.meta.url), "utf8"); const webShell = await readFile(new URL("../mac-test.html", import.meta.url), "utf8"); const frameDriverSource = await readFile(new URL("../frame-driver.mjs", import.meta.url)); const socialRenderer = await readFile(new URL( @@ -106,9 +106,13 @@ function createFight(startImmediately = true, enterGame = true, ); globalThis.__oskiewarRoundBridge = roundBridge; globalThis.__oskiewarAttractVariant = "still"; + const opponentOverride = globalThis.__oskiewarOpponent; + if (opponentOverride === undefined) globalThis.__oskiewarOpponent = "dummy"; fight.boot(); globalThis.__oskiewarRoundBridge = null; delete globalThis.__oskiewarAttractVariant; + if (opponentOverride === undefined) delete globalThis.__oskiewarOpponent; + else globalThis.__oskiewarOpponent = opponentOverride; if (enterGame) fight.enterGame(); const tick = (elapsedUs = 16667) => { @@ -184,6 +188,36 @@ test("mobile controls live in the canvas rather than DOM buttons", () => { assert.match(source, /drawTouchControls\(\);/); }); +test("touch controls need no duplicate tutorial legend", () => { + const legend = source.slice(source.indexOf("function drawControlLegend"), + source.indexOf("function drawTouchControls")); + assert.match(legend, /capabilities\(\)\.inputFamily === "touch"\) return/); + assert.match(webShell, /\.touch-enabled #account/); +}); + +test("phone action buttons speak the canonical command stream", () => { + const controls = source.slice(source.indexOf("function drawTouchControls"), + source.indexOf("function keycapRunWidth")); + assert.match(controls, /commandGlyph = \{ A: "\/", B: "\*", X: "\)", Y: "\+" \}/); + assert.match(controls, /commandGlyph\[key\]/); +}); + +test("phone names shrink and the PDT clock fits beside the notch", () => { + assert.match(webShell, /viewport-fit=cover/); + assert.match(source, /const size = touch \? 24 : hudTypeSize/); + assert.match(source, /const nameSize = touch \? 32 : compactLayout\(\) \? 48 : 74/); + const clock = source.slice(source.indexOf("function hudClockBox"), + source.indexOf("function hudStatusTray")); + assert.match(clock, /const size = touch \? 16 : hudTypeSize/); + assert.match(clock, /left: right - handleWidth\(label, size\)/); +}); + +test("the iOS shell can pulse debug without inventing another input", () => { + assert.match(webShell, /globalThis\.__oskiewarToggleDebug = \(\) =>/); + assert.match(webShell, /keys\[0\]\.add\("View"\)/); + assert.match(webShell, /keys\[0\]\.delete\("View"\)/); +}); + test("every combat button is reachable from a keyboard on both pads", () => { const keymap = /const keyMap = new Map\(\[([\s\S]*?)\]\);/.exec(webShell)[1]; const bound = [...keymap.matchAll(/\[(\d), "([A-Za-z]+)"\]/g)] @@ -308,7 +342,7 @@ test("a detected pad swaps the round legend onto gamepad wording", () => { assert.equal(host.capabilities().inputFamily, "xbox"); const { fight } = createFight(false, false, "xbox-uwp"); assert.equal(fight.combatLegend(fight.players[0]), - "A KICK B PUNCH X SHIELD Y USE ITEM STICK JUMP"); + "LEFT,RIGHT MOVE RIGHT,RIGHT >> DASH A KICK B PUNCH X SHIELD Y USE ITEM STICK JUMP"); }); test("every instructional keyboard key uses the shared gray keycap renderer", () => { @@ -538,7 +572,7 @@ test("title shows a live Pacific clock and version timestamp", () => { const { fight } = createFight(false, false); assert.match(fight.pacificTimeLabel(1785870000000), /^\d{1,2}:\d{2}(am|pm) P(D|S)T$/); - assert.match(source, /const buildTimestamp = "\d{4}\.\d{2}\.\d{2}\.\d{4} PDT"/); + assert.match(source, /const buildVersion = 500/); assert.match(source, /const clock = hudClockBox\(titleUnixMs\)/); assert.match(source, /typeWrite\(clock\.label, clock\.left, safe\.top \+ 2/); assert.match(source, /const fpsLabel = Math\.round\(displayFps \|\| 0\)/); @@ -974,6 +1008,38 @@ test("the killcam keeps the winner in frame instead of deleting them", () => { assert.doesNotMatch(source, /if \(!player\.alive && !roundResult\) return;/); }); +test("a dismembered diving winner keeps animating through the KO shot", () => { + const { fight, tick } = createFight(); + const winner = fight.players[0]; + winner.removedParts = ["left-arm", "right-leg"]; + winner.y = 1800; + winner.vy = 420; + winner.grounded = false; + const before = winner.y; + fight.knockOut(); + tick(); + assert.equal(winner.frozenGeometry, undefined); + assert.ok(winner.y > before, "winner should continue the dive under neutral physics"); + assert.ok(fight.runnerWorldGeometry(winner, 0).segments + .every((segment) => !winner.removedParts.includes(segment.part))); + assert.match(source, /updatePlayer\(cinematicWinner,/); + assert.match(source, /updateDetachedParts\(dt\)/); + assert.match(source, /const victoryAmount = deathCinematic\?\.winnerPad/); + assert.match(source, /Math\.sign\(defeated\.x - player\.x\)/); +}); + +test("hilly terrain shares one deterministic surface with physics and foot IK", () => { + assert.match(source, /function terrainFloorAt\(x\)/); + assert.match(source, /const broad = Math\.sin/); + assert.match(source, /const detail = Math\.sin/); + assert.match(source, /function drawTerrainSurface\(/); + assert.match(source, /player\.y >= terrainFloorAt\(player\.x\)/); + assert.match(source, /const leadGround = terrainFloorAt\(leadFoot\)/); + assert.match(source, /const rearGround = terrainFloorAt\(rearFoot\)/); + assert.match(source, /terrainFloorAt\(ball\.x\) - ball\.radius/); + assert.match(source, /terrainPhase = terrainSeed\("oskiewar-physics-1-hills"\)/); +}); + test("wind flag lives on the platform without an MPH HUD label", { skip: !WIND_FLAG }, () => { assert.match(source, /const poleBottom = platformY/); assert.match(source, /const poleX = \(platformLeft \+ platformRight\) \/ 2/); @@ -1038,6 +1104,35 @@ test("debug HUD shows native surface and timing without repeating oskiewar", () assert.doesNotMatch(source, /const gameLabel = "oskiewar"/); }); +test("the round timer cannot leak through the start screen", () => { + assert.match(source, /let gameplayStarted = false/); + assert.match(source, /function beginTraining[\s\S]{0,180}gameplayStarted = false/); + const enter = source.slice(source.indexOf("function enterGame"), + source.indexOf("function updateShell")); + assert.match(enter, /gameplayStarted = true/); + assert.match(source, /if \(shellMode === "GAME" && gameplayStarted\) \{/); +}); + +test("starting after action attract becomes player versus dummy", () => { + const enter = source.slice(source.indexOf("function enterGame"), + source.indexOf("function updateShell")); + assert.match(enter, /players\[0\]\.bot = false/); + assert.match(enter, /players\[1\]\.npc = true/); + assert.match(enter, /players\[1\]\.bot = false/); + assert.match(enter, /players\[1\]\.spiderDummy = false/); +}); + +test("raw left stick gives pupils subtle pre-deadzone gaze", () => { + const face = source.slice(source.indexOf("function drawFace"), + source.indexOf("function drawInventory")); + assert.match(face, /const movementDeadzone = \.48/); + assert.match(face, /const gazeNoiseFloor = \.015/); + assert.match(face, /const rawMagnitude = Math\.hypot\(rawX, rawY\)/); + assert.match(face, /movementDeadzone - gazeNoiseFloor/); + assert.match(face, /lerp\(direction \* \.2, rawX, gazeAmount\)/); + assert.doesNotMatch(face, /Math\.abs\(rawX\) >= deadzone/); +}); + // The bug is a large bottom-center global-mode indicator. MIDI remains in its // own clock-side peripheral tray. test("debug starts hidden and shows its enlarged bottom-center bug", () => { @@ -2263,6 +2358,33 @@ test("spiderdummy is a giant inert foe with destructible segmented legs", () => part.part === "left-arm").length >= 6); }); +test("spiderdummy remains directly reachable while pal select is retired", () => { + globalThis.__oskiewarOpponent = "spiderdummy"; + const { fight } = createFight(false, false); + delete globalThis.__oskiewarOpponent; + assert.equal(fight.players[1].name, "SPIDERDUMMY"); + assert.equal(fight.players[1].spiderDummy, true); + assert.match(webShell, /get\("opponent"\)/); +}); + +test("fresh training sessions choose only dummy or spiderdummy 50/50", () => { + const choice = source.slice(source.indexOf("function trainingOpponentKind"), + source.indexOf("const selectionReady")); + assert.match(choice, /requested === "dummy" \|\| requested === "spiderdummy"/); + assert.match(choice, /hashUnit\("training-opponent:"/); + assert.match(choice, /< \.5\s*\? "dummy" : "spiderdummy"/); + assert.doesNotMatch(choice, /\bbot\b/); +}); + +test("skateboard is a deterministic physical match prop with native geometry", () => { + assert.match(source, /\{ type: "skateboard", spawnOwner: -1, radius: 52/); + const drawSource = source.slice(source.indexOf("function drawBall"), + source.indexOf("function drawBallHitboxes")); + assert.match(drawSource, /ball\.type === "skateboard"/); + assert.match(drawSource, /filledCapsule/); + assert.match(drawSource, /filledDisc/); +}); + test("losing both legs grounds the pelvis in a low crouched form", () => { const { fight, now } = createFight(); const target = fight.players[1]; @@ -2523,7 +2645,7 @@ function jumpArc(harness, pad = 0, holdFrames = 200) { // Clear of the ledge: it now sits inside a plain jump's arc, and landing on // it mid-measurement would truncate both the apex and the airtime. const stage = harness.fight.stageGeometry(); - player.x = stage.platformLeft - 900; + if (PLATFORM) player.x = stage.platformLeft - 900; const floor = player.y; const startedAt = harness.now(); let liftAt = 0; @@ -2660,10 +2782,11 @@ test("a lone crouch tap on the platform never sinks", () => { test("double-tapping crouch on the floor still fast-drops instead of sinking", () => { const { fight, tap } = createFight(); const player = fight.players[0]; + const ground = player.y; tap(0, "ArrowDown"); tap(0, "ArrowDown"); assert.equal(player.lastButton, "DASH DOWN"); - assert.equal(player.y, 12000); + assert.equal(player.y, ground); }); test("a bot stranded on the platform sinks back into reach", { skip: !PLATFORM }, () => { @@ -3179,13 +3302,17 @@ test("owned gun and grenade counts render above each bottom handle", () => { assert.match(source, /function drawHudInventory\(player, side\)/); assert.match(source, /items\.push\("gun " \+ player\.gunAmmo\)/); assert.match(source, /items\.push\("grenade " \+ player\.grenadeAmmo\)/); - assert.match(source, - /drawHudInventory\(players\[0\], 0\);\n drawHudInventory\(players\[1\], 1\);/); + assert.match(source, /drawHudInventory\(player, side\)/); const { fight } = createFight(); fight.players[0].gunAmmo = 3; assert.ok(fight.runnerWorldGeometry(fight.players[0], 0).segments .some((segment) => segment.role === "item-forearm")); assert.doesNotMatch(source, /gunPips|grenadePips/); + const inventorySource = source.slice(source.indexOf("function drawInventory"), + source.indexOf("function drawDigitalHeadBurst")); + assert.match(inventorySource, + /filledCapsule\(hand\.x, hand\.y, barrel\.x, barrel\.y/); + assert.doesNotMatch(inventorySource, /\bline\(/); }); test("player animation state reads as a boxed syntax diagram", () => { @@ -3196,19 +3323,23 @@ test("player animation state reads as a boxed syntax diagram", () => { const lines = fight.playerStatLines(player); assert.equal(lines.length, 3); assert.match(lines[0], /^p1 :: /); - assert.match(lines[1], /^in\[1,0\] -> stk\[0\.00\] vx\[-?\d+\]$/); - assert.match(lines[2], /^anim::\w+ step\[\d+\/\d+\] t\[\d+\]$/); + assert.match(lines[1], /^in\[ 1, 0\] -> stk\[ 0\.00\] vx\[\s*-?\d+\]$/); + assert.match(lines[2], /^anim::\w+\s+ step\[\s*\d+\/\s*\d+\] t\[\s*\d+\]$/); const handle = fight.playerHandleLayout(player, 0); assert.equal(handle.size, 42); fight.setDebugHitboxes(true); assert.ok(fight.statStackHeight() > 80); fight.setDebugHitboxes(false); assert.equal(fight.statStackHeight(), 0); - assert.match(source, /strokeBox\(x, y, width, height, 2, edge\)/); - assert.match(source, /for \(let step = 0; step < count; step\+\+\)/); - assert.match(source, /const active = step <= Math\.min\(count - 1, animation\.step\)/); - assert.match(source, - /drawPlayerStats\(players\[0\], 0, t\);\n drawPlayerStats\(players\[1\], 1, t\);/); + assert.match(source, /triangleDepth = -1\.445/); + assert.match(source, /screenStrokeRect\(x, y, width, height, 2, edge\)/); + assert.doesNotMatch(source.slice(source.indexOf("function drawPlayerStats"), + source.indexOf("function drawHudInventory")), /\bbox\(/); + assert.match(source, /const width = compactLayout\(\) \? 360 : 520/); + assert.doesNotMatch(source.slice(source.indexOf("function drawPlayerStats"), + source.indexOf("function drawHudInventory")), /const track|segmentWidth/); + assert.match(source, /const hudPlayers = spatialHudPlayers\(\)/); + assert.match(source, /drawPlayerStats\(player, side, t\)/); assert.match(source, /const handle = playerHandleLayout\(player, side\)/); assert.match(source, /handle\.y - height - 12/); // The read-out left the world; nothing labels the fighters any more. @@ -3216,6 +3347,48 @@ test("player animation state reads as a boxed syntax diagram", () => { source.indexOf("function drawPlayerHud")), /typeWrite/); }); +test("HUD ownership follows fighter position with crossing hysteresis", () => { + assert.match(source, /function spatialHudPlayers\(\)/); + assert.match(source, /if \(other\.x < left\.x - 36\) hudLeftPad = other\.pad/); + assert.match(source, /drawPlayerHandle\(player, t, side\)/); + assert.match(source, /drawCommandStream\(player, side\)/); +}); + +test("persistent controls teach the two-button hold chord", () => { + assert.match(source, /both \? "GRAB" : held\.includes\("A"\) \? "KICK"/); + assert.match(source, /!both && held\.includes\("B"\) \? "PUNCH"/); + assert.doesNotMatch(source, /= HOLD/); + assert.match(source, /safe\.top \+ row \* step/); + assert.match(source, /const controlRailWidth/); + assert.match(source, /pad\.leftX < -\.12/); + assert.match(source, /pad\.leftX > \.12/); + assert.match(source, /pad\.leftY > \.12/); + assert.match(source, /const step = Math\.round\(size \* 1\.82\)/); + assert.match(source, /\[\["RIGHT", "RIGHT"\], ">> DASH"/); + assert.match(source, /\[\["LEFT", "RIGHT"\], "MOVE"/); + assert.match(source, /Object\.hasOwn\(padGlyph, label\)/); + assert.match(source, + /label === "STICK_UP" \|\| label === "LEFT" \|\| label === "RIGHT"/); +}); + +test("handle palette gradients survive on detached limbs and anchor the head at @", () => { + assert.match(source, /function drawPaletteCapsule\(/); + assert.match(source, /const bands = colors\?\.length > 1 \? 6 : 1/); + assert.match(source, /player\.handleColors\[0\] : color/); + assert.match(source, /colors: player\.npc \? \[\] : player\.handleColors/); + assert.match(source, /paletteCoordinate/); +}); + +test("dummy state card describes an inert destructible target", () => { + const { fight } = createFight(); + fight.startFightAgainst("dummy"); + const lines = fight.playerStatLines(fight.players[1]); + assert.deepEqual(lines.length, 3); + assert.match(lines[0], /^p2 :: training dummy$/); + assert.match(lines[1], /^target::inert parts\[ 5\/ 5\] dmg\[ 0\]$/); + assert.doesNotMatch(lines.join(" "), /stk|vx/); +}); + test("diagnostic lines color handles, numbers, units, labels, and punctuation", () => { const { fight } = createFight(false, false); const line = "@jeffrey vx -240, stk 0.50"; @@ -3684,14 +3857,14 @@ test("legacy Xbox drum allowlists cannot stop bell or whoosh cues", () => { test("client failures become an on-screen error state", () => { const { fight, telemetryEvents, setHostErrorStatus } = createFight(false, false); const error = new RangeError("unknown drum"); - error.stack = "RangeError: unknown drum\n at playDrum (hello.js:724:9)"; + error.stack = "RangeError: unknown drum\n at playDrum (oskiewar.js:724:9)"; fight.captureClientError("sim", error); assert.match(fight.clientErrorState(), /sim: RangeError: unknown drum/); assert.equal(fight.clientErrorDetailState().source.line, 724); const payload = JSON.parse(telemetryEvents.find(([event]) => event === "CLIENT_ERROR")[1]); assert.equal(payload.phase, "sim"); - assert.equal(payload.source.file, "hello.js"); + assert.equal(payload.source.file, "oskiewar.js"); assert.equal(payload.state.players.length, 2); setHostErrorStatus("posted to server xbox-oskiewar-test"); assert.equal(fight.errorReportStatus(), "posted to server xbox-oskiewar-test"); @@ -3733,7 +3906,8 @@ test("the error screen carries its whole dump in a scannable QR link", () => { assert.equal(dump.src.line, 1162); assert.match(dump.k, /screenTriangle/); assert.equal(dump.s.players.length, 2); - assert.equal(dump.s.build, /const buildTimestamp = "([^"]+)"/.exec(source)[1]); + assert.equal(dump.s.build, "v" + + Number(/const buildVersion = (\d+)/.exec(source)[1])); assert.ok(dumpRows(dump).some(([label]) => label === "camera")); assert.doesNotThrow(() => fight.paint()); assert.match(source, /errorTypeWrite\("scan to share this dump"/); diff --git a/xbox/live/tests/renderer.test.mjs b/xbox/live/tests/renderer.test.mjs index d6c869208e..15c3b2a788 100644 --- a/xbox/live/tests/renderer.test.mjs +++ b/xbox/live/tests/renderer.test.mjs @@ -1,6 +1,6 @@ // The rasterization front end, on its own bench. // -// `hello.test.mjs` drives whole frames; this file drives the camera and the +// `oskiewar.test.mjs` drives whole frames; this file drives the camera and the // clipper directly, because the artifacts they produce are geometric and a // frame is too coarse a thing to see them in. Everything here loads the same // source through the same sandbox, but only reaches for the projection and @@ -9,7 +9,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; -const source = await readFile(new URL("../hello.js", import.meta.url), "utf8"); +const source = await readFile(new URL("../oskiewar.js", import.meta.url), "utf8"); // One face per emitted triangle, with the vertices as the host receives them. function createRenderer({ width = 1920, height = 1080 } = {}) { diff --git a/xbox/live/tests/replay-compat.test.mjs b/xbox/live/tests/replay-compat.test.mjs new file mode 100644 index 0000000000..cd8f76aeb9 --- /dev/null +++ b/xbox/live/tests/replay-compat.test.mjs @@ -0,0 +1,72 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; +import { + inputTimeline, migrateReplay, playbackPlan, replayDigest, + simulationContracts, timelineDigest, validateCompatibleReplay, + validateFixtureManifest, +} from "../replay/compat.mjs"; + +const fixture = async (name) => JSON.parse(await readFile(new URL( + `../replay/fixtures/${name}.json`, import.meta.url), "utf8")); + +test("an old build migrates without changing its deterministic input ticks", async () => { + const old = await fixture("legacy-v0"); + const migrated = migrateReplay(old); + assert.equal(migrated.version, 1); + assert.equal(migrated.simulation, "oskiewar-physics-1"); + assert.deepEqual(inputTimeline(old), [ + [0, 2, 0], [1, 2, 0], [2, 2, 16], [3, 2, 16], [4, 0, 16], + [5, 0, 16], [6, 0, 16], [7, 0, 0], [8, 0, 0], + ]); + assert.equal(timelineDigest(old), + "a6d46e00073439d314fce7d278a2827078a9fcedc6c1eb3eb22c01e34aea330b"); +}); + +test("playback is pinned to an archived simulation, not the current build", async () => { + const old = await fixture("legacy-v0"); + const plan = playbackPlan(old, 900); + assert.equal(plan.recordingBuild, 37); + assert.equal(plan.playerBuild, 900); + assert.equal(plan.simulation, "oskiewar-physics-1"); + assert.equal(plan.clock.tickUs, 16667); + assert.equal(plan.timeline.length, 9); +}); + +test("unknown old physics fail loudly instead of playing inaccurately", async () => { + const demo = await fixture("v1"); + demo.simulation = "oskiewar-physics-0-lost"; + assert.deepEqual(validateCompatibleReplay(demo), { + ok: false, error: "Archived simulation unavailable: oskiewar-physics-0-lost", + }); + assert.throws(() => playbackPlan(demo, 500), /Archived simulation unavailable/); +}); + +test("current envelope has a stable canonical and timeline digest", async () => { + const demo = await fixture("v1"); + assert.equal(simulationContracts[demo.simulation].inputEncoding, + "oskiewar-buttons-v1"); + assert.equal(replayDigest(demo), + "ca36d27675af75123121de9b3a72af1af2a7bb21e6ade38798ce2ce470022437"); + assert.equal(timelineDigest(demo), + "865d88c02a15a9fb9328f674e2811cee08f7971edb724fbb16df2ad2760b98c5"); +}); + +test("wall-clock order cannot alter deterministic command order", async () => { + const demo = await fixture("v1"); + demo.commands = [[2, 0, 1], [1, 0, 0]]; + assert.equal(validateCompatibleReplay(demo).error, + "Command timeline is not monotonic"); +}); + +test("versioned fixture ledger pins old recordings and compatibility builds", async () => { + const manifest = await fixture("manifest"); + assert.equal(validateFixtureManifest(manifest).ok, true); + for (const item of manifest.fixtures) { + const demo = await fixture(item.file.replace(/\.json$/, "")); + assert.equal(demo.build, item.recordingBuild); + assert.equal(replayDigest(demo), item.replayDigest); + assert.equal(timelineDigest(demo), item.timelineDigest); + assert.doesNotThrow(() => playbackPlan(demo, item.minimumPlayerBuild)); + } +}); diff --git a/xbox/live/tests/scene3d-webgl.test.mjs b/xbox/live/tests/scene3d-webgl.test.mjs new file mode 100644 index 0000000000..1bf941b3ec --- /dev/null +++ b/xbox/live/tests/scene3d-webgl.test.mjs @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const source = await readFile(new URL("../scene3d-webgl.mjs", import.meta.url), + "utf8"); + +test("web scene renderer requests and clears a real depth buffer", () => { + assert.match(source, /getContext\("webgl2"/); + assert.match(source, /depth:\s*true/); + assert.match(source, /enable\(gl\.DEPTH_TEST\)/); + assert.match(source, /depthFunc\(gl\.LEQUAL\)/); + assert.match(source, /COLOR_BUFFER_BIT\s*\|\s*gl\.DEPTH_BUFFER_BIT/); +}); +test("web scene renderer streams one interleaved triangle buffer", () => { + assert.match(source, /bufferSubData\(gl\.ARRAY_BUFFER/); + assert.match(source, /drawArrays\(gl\.TRIANGLES/); +}); diff --git a/xbox/live/tests/scene3d.test.mjs b/xbox/live/tests/scene3d.test.mjs new file mode 100644 index 0000000000..e9807f1bc9 --- /dev/null +++ b/xbox/live/tests/scene3d.test.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + clipDepth, + clipPoint, + OskiewarScene3D, + OSKIEWAR_TRIANGLE_FLOATS, +} from "../scene3d.mjs"; + +test("scene depth matches the shipped Xbox D3D mapping", () => { + assert.equal(clipDepth(-1.5), 0); + assert.equal(clipDepth(0), .5); + assert.equal(clipDepth(1.5), 1); + assert.equal(clipDepth(99), 1); +}); +test("logical coordinates become GPU clip coordinates", () => { + assert.deepEqual(clipPoint(0, 0, 0, 1920, 1080), [-1, 1, .5]); + assert.deepEqual(clipPoint(960, 540, 0, 1920, 1080), [0, 0, .5]); + assert.deepEqual(clipPoint(1920, 1080, 0, 1920, 1080), [1, -1, .5]); +}); + +test("scene batches depth-bearing colored triangles without allocation churn", () => { + const scene = new OskiewarScene3D({ maxTriangles: 2 }); + scene.beginFrame(); + assert.equal(scene.triangle(0, 0, -1, 10, 0, 0, 0, 10, 1, + 255, 128, 0, 20, 10), true); + assert.equal(scene.triangleCount, 1); + assert.equal(scene.frameVertices().length, OSKIEWAR_TRIANGLE_FLOATS); + assert.equal(scene.frameVertices()[3], 1); + assert.ok(Math.abs(scene.frameVertices()[4] - 128 / 255) < 1e-6); + assert.equal(scene.frameVertices()[5], 0); + scene.beginFrame(); + assert.equal(scene.triangleCount, 0); + assert.equal(scene.frameVertices().length, 0); +}); diff --git a/xbox/mac-test/main.swift b/xbox/mac-test/main.swift index 638b447e6f..d92f72f357 100644 --- a/xbox/mac-test/main.swift +++ b/xbox/mac-test/main.swift @@ -10,7 +10,7 @@ final class FightAppDelegate: NSObject, NSApplicationDelegate { } let live = resources.appendingPathComponent("live", isDirectory: true) let html = live.appendingPathComponent("mac-test.html") - let piece = live.appendingPathComponent("hello.js") + let piece = live.appendingPathComponent("oskiewar.js") guard let pieceSource = try? String(contentsOf: piece, encoding: .utf8), let encoded = try? JSONSerialization.data(withJSONObject: [pieceSource]), let sourceArray = String(data: encoded, encoding: .utf8) else { diff --git a/xbox/macos-native/README.md b/xbox/macos-native/README.md index 27e918354a..0e621bc1a6 100644 --- a/xbox/macos-native/README.md +++ b/xbox/macos-native/README.md @@ -1,7 +1,7 @@ # oskiewar for macOS The installed macOS game is an AppKit application with no WebView. JavaScriptCore -runs the same `xbox/live/hello.js` lifecycle as Xbox; AppKit/CoreGraphics, +runs the same `xbox/live/oskiewar.js` lifecycle as Xbox; AppKit/CoreGraphics, AVFoundation, and GameController provide native graphics, audio, and input. ```bash diff --git a/xbox/macos-native/main.swift b/xbox/macos-native/main.swift index 1053dbb4f2..34550a05f3 100644 --- a/xbox/macos-native/main.swift +++ b/xbox/macos-native/main.swift @@ -24,6 +24,7 @@ private enum DrawCommand { case box(CGRect, Ink) case line(CGPoint, CGPoint, CGFloat, Ink) case triangle(CGPoint, CGPoint, CGPoint, Ink) + case triangle3d(CGPoint, CGFloat, CGPoint, CGFloat, CGPoint, CGFloat, Ink) case text(String, CGPoint, CGFloat, Ink, String) } @@ -403,6 +404,18 @@ private final class GameView: NSView { context.closePath() context.setFillColor(ink.color.cgColor) context.fillPath() + case let .triangle3d(first, _, second, _, third, _, ink): + // AppKit remains the compatibility renderer. It deliberately + // ignores depth here; Metal/SDL hosts consume the same command + // with a real depth attachment instead of erasing z at the JS + // boundary as this host did previously. + context.beginPath() + context.move(to: first) + context.addLine(to: second) + context.addLine(to: third) + context.closePath() + context.setFillColor(ink.color.cgColor) + context.fillPath() case let .text(text, point, size, ink, family): let font = NSFont(name: family, size: size) ?? NSFont.systemFont(ofSize: size) let attributes: [NSAttributedString.Key: Any] = [ @@ -467,7 +480,7 @@ private final class NativeGameHost { GCController.startWirelessControllerDiscovery(completionHandler: nil) guard let resources = Bundle.main.resourceURL, let hello = try? String(contentsOf: - resources.appendingPathComponent("live/hello.js"), encoding: .utf8), + resources.appendingPathComponent("live/oskiewar.js"), encoding: .utf8), var qr = try? String(contentsOf: resources.appendingPathComponent("live/qr.mjs"), encoding: .utf8) else { javascriptError = "missing native game resources" @@ -478,7 +491,7 @@ private final class NativeGameHost { range: NSRange(qr.startIndex..., in: qr), withTemplate: "\n") } javascript.evaluateScript(qr + "\n" + hello, - withSourceURL: URL(fileURLWithPath: "oskiewar/hello.js")) + withSourceURL: URL(fileURLWithPath: "oskiewar/oskiewar.js")) call("boot") // A title screen barely simulates anything, so measuring one tells you // nothing. Self-play drops straight into a real fight. @@ -644,9 +657,11 @@ private final class NativeGameHost { CGPoint(x: x3, y: y3), Ink(r, g, b))) } let triangle3d: @convention(block) (Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double, Double) -> Void = - { [weak self] x1, y1, _, x2, y2, _, x3, y3, _, r, g, b in - self?.renderer.commands.append(.triangle(CGPoint(x: x1, y: y1), CGPoint(x: x2, y: y2), - CGPoint(x: x3, y: y3), Ink(r, g, b))) + { [weak self] x1, y1, z1, x2, y2, z2, x3, y3, z3, r, g, b in + self?.renderer.commands.append(.triangle3d( + CGPoint(x: x1, y: y1), CGFloat(z1), + CGPoint(x: x2, y: y2), CGFloat(z2), + CGPoint(x: x3, y: y3), CGFloat(z3), Ink(r, g, b))) } let text: (String) -> @convention(block) (String, Double, Double, Double, Double, Double, Double) -> Void = { family in { [weak self] value, x, y, size, r, g, b in diff --git a/xbox/steam/STEAM.md b/xbox/steam/STEAM.md index bf9b36fe91..3285395fb1 100644 --- a/xbox/steam/STEAM.md +++ b/xbox/steam/STEAM.md @@ -154,13 +154,13 @@ Three runtimes for one game file, in very different states of Steam-readiness: | Runtime | State | Steam fit | |---|---|---| -| **Browser** — `mac-test.html` + `frame-driver.mjs` + `hello.js` | The most-exercised runtime in the repo: the reel factory renders it three times a day, the blackbox tests drive it headlessly, the social-preview burner hashes it. Chromium's Gamepad API already plays it. | Ships anywhere Chromium ships. | +| **Browser** — `mac-test.html` + `frame-driver.mjs` + `oskiewar.js` | The most-exercised runtime in the repo: the reel factory renders it three times a day, the blackbox tests drive it headlessly, the social-preview burner hashes it. Chromium's Gamepad API already plays it. | Ships anywhere Chromium ships. | | **macOS native** — `xbox/macos-native/main.swift` | 806 lines of AppKit/JavaScriptCore, no WebView: CoreGraphics rasterizes the draw commands, AVAudioEngine synthesizes the drums, GameController reads pads. Installed and working today. | Steam-shippable for macOS after a Steamworks binding — but macOS only. | | **Windows native** — `xbox/native-bios/` | QuickJS + D3D11 + XAudio2 under **UWP** (C++/CX, CoreWindow). | Steam ships plain Win32 executables, not UWP/MSIX packages. `PUBLISHING.md` already established this platform layer is a rewrite for any non-UWP target; Steam does not change that. There is no Linux shell at all. | ### The three options -**Electron.** Wrap the browser runtime — the exact `hello.js` + frame-driver +**Electron.** Wrap the browser runtime — the exact `oskiewar.js` + frame-driver pair the factory verifies daily — in Electron for Windows, macOS, and Linux from one shell. [`steamworks.js`](https://github.com/ceifa/steamworks.js/) provides Steamworks bindings with a documented Electron path, including diff --git a/xbox/steam/store-page/copy.md b/xbox/steam/store-page/copy.md index 05974620ca..7d7c79ec05 100644 --- a/xbox/steam/store-page/copy.md +++ b/xbox/steam/store-page/copy.md @@ -87,7 +87,7 @@ to him. Or the bot, who does not block for your benefit. | Claim | Why it stays out | |---|---| | Online multiplayer / netplay | Does not exist. The network layer is one-way spectator publishing and replay upload. Remote Play Together is the honest version of this sentence. | -| Best of five | `hello.js` carries `matchWins = 5` but nothing accumulates round wins — a match is a round today (`xbox/live/MARKETING.md`). If that changes, the copy can too. | +| Best of five | `oskiewar.js` carries `matchWins = 5` but nothing accumulates round wins — a match is a round today (`xbox/live/MARKETING.md`). If that changes, the copy can too. | | "24 seconds on the clock" | The retro reel hook says 24; the sim says `roundDurationUs = 30000000`. The copy just says a round clock and stays right either way. | | Deterministic replays you can browse | True on the web (`/api/oskiewar-replays`), but not a shipped Steam-build feature until the shell exposes it. Determinism itself is safe to mention; a replay browser is not. | | Achievements | Add the category and a line only once the list is designed and wired. | diff --git a/xbox/test-native.sh b/xbox/test-native.sh index f579f9fcdd..0bebcd6121 100644 --- a/xbox/test-native.sh +++ b/xbox/test-native.sh @@ -6,7 +6,7 @@ build_dir=$(mktemp -d "${TMPDIR:-/tmp}/ac-xbox-tests.XXXXXX") trap 'rm -rf "$build_dir"' EXIT INT TERM node --check "$repo_dir/xbox/live/controller-probe.js" -node --check "$repo_dir/xbox/live/hello.js" +node --check "$repo_dir/xbox/live/oskiewar.js" node --check "$repo_dir/xbox/live/native-showcase.js" node "$repo_dir/xbox/live/tests/controller-probe.test.mjs" node "$repo_dir/xbox/tools/tests/kidlisp-native.test.mjs" diff --git a/xbox/tools/build-macos-app.sh b/xbox/tools/build-macos-app.sh index 39330eb1f5..771f333fa7 100755 --- a/xbox/tools/build-macos-app.sh +++ b/xbox/tools/build-macos-app.sh @@ -15,7 +15,7 @@ swiftc -swift-version 5 -O "$repo_root/xbox/macos-native/main.swift" \ -framework GameController -framework JavaScriptCore \ -o "$contents/MacOS/oskiewar" cp "$repo_root/xbox/macos-native/Info.plist" "$contents/Info.plist" -cp "$repo_root/xbox/live/hello.js" "$contents/Resources/live/hello.js" +cp "$repo_root/xbox/live/oskiewar.js" "$contents/Resources/live/oskiewar.js" cp "$repo_root/system/public/aesthetic.computer/dep/@akamfoad/qr/qr.mjs" \ "$contents/Resources/live/qr.mjs" cp "$repo_root/system/public/papers.aesthetic.computer/foundry/fonts/ComicRelief-Regular.ttf" \ diff --git a/xbox/tools/live.mjs b/xbox/tools/live.mjs index b8312a68c3..3214989cfd 100644 --- a/xbox/tools/live.mjs +++ b/xbox/tools/live.mjs @@ -136,6 +136,9 @@ function launch() { function publish(sourcePath) { if (!sourcePath) throw new Error("usage: xbox-live publish "); const absolute = resolve(sourcePath); + if (basename(absolute) === "oskiewar.js" && + process.env.OSKIEWAR_UNIFIED_DEPLOY !== "1") + throw new Error("canonical Oskiewar deploys must use npm run oskiewar:deploy"); if (!existsSync(absolute)) throw new Error(`piece not found: ${absolute}`); let source = readFileSync(absolute, "utf8"); if (source.startsWith("// @bundle-qr")) { diff --git a/xbox/tools/mac-test-app.sh b/xbox/tools/mac-test-app.sh index 1135efed83..f6ab72e76c 100644 --- a/xbox/tools/mac-test-app.sh +++ b/xbox/tools/mac-test-app.sh @@ -12,7 +12,7 @@ swiftc -O "$repo_root/xbox/mac-test/main.swift" \ -o "$contents/MacOS/Oskiewar" cp "$repo_root/xbox/mac-test/Info.plist" "$contents/Info.plist" cp "$repo_root/xbox/live/mac-test.html" "$contents/Resources/live/mac-test.html" -cp "$repo_root/xbox/live/hello.js" "$contents/Resources/live/hello.js" +cp "$repo_root/xbox/live/oskiewar.js" "$contents/Resources/live/oskiewar.js" cp "$repo_root/system/public/papers.aesthetic.computer/foundry/fonts/ComicRelief-Regular.ttf" \ "$contents/Resources/live/ComicRelief-Regular.ttf" diff --git a/xbox/tools/oskiewar-release.mjs b/xbox/tools/oskiewar-release.mjs new file mode 100644 index 0000000000..4c0ff56860 --- /dev/null +++ b/xbox/tools/oskiewar-release.mjs @@ -0,0 +1,173 @@ +#!/usr/bin/env node +// One release receipt for Oskiewar's web, iOS-web, and Xbox live surfaces. + +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const sourcePath = resolve(root, "xbox/live/oskiewar.js"); +const receiptPath = resolve(root, ".git/oskiewar-parity.json"); +const channels = ["web", "ios", "xbox"]; + +export const sha256 = (value) => createHash("sha256").update(value).digest("hex"); + +export function classifySeverity(paths) { + const iosNative = paths.some((path) => path.startsWith("apple/oskiewar/") && + !path.endsWith("oskiewar.js")); + const xboxNative = paths.some((path) => path.startsWith("xbox/native-bios/") || + path.startsWith("xbox/package/")); + if (iosNative && xboxNative) return "multi-native"; + if (iosNative) return "ios-native"; + if (xboxNative) return "xbox-native"; + return "live"; +} + +export function newRelease(hash, commit, severity, previous = null) { + const at = new Date().toISOString(); + return { + format: "computer.aesthetic.oskiewar-parity", version: 1, + desired: { hash, commit, severity, createdAt: at }, + channels: Object.fromEntries(channels.map((name) => [name, { + status: previous?.channels?.[name]?.hash === hash ? "current" : "pending", + hash: previous?.channels?.[name]?.hash || null, updatedAt: at, + }])), + }; +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { cwd: root, encoding: "utf8", + stdio: options.capture ? "pipe" : "inherit", + env: { ...process.env, OSKIEWAR_UNIFIED_DEPLOY: "1", ...options.env } }); + if (result.status !== 0) throw new Error((result.stderr || "").trim() || + `${command} exited ${result.status}`); + return result.stdout || ""; +} + +function git(...args) { return run("git", args, { capture: true }).trim(); } +function readReceipt() { + if (!existsSync(receiptPath)) return null; + try { return JSON.parse(readFileSync(receiptPath, "utf8")); } catch { return null; } +} +function save(receipt) { + mkdirSync(dirname(receiptPath), { recursive: true }); + const temporary = `${receiptPath}.${process.pid}`; + writeFileSync(temporary, JSON.stringify(receipt, null, 2) + "\n"); + renameSync(temporary, receiptPath); +} +function mark(receipt, name, status, detail = "") { + receipt.channels[name] = { status, + hash: status === "current" ? receipt.desired.hash : receipt.channels[name]?.hash || null, + updatedAt: new Date().toISOString(), ...(detail ? { detail } : {}) }; + save(receipt); +} + +function sourceState(previous = null) { + if (!existsSync(sourcePath)) throw new Error(`missing ${sourcePath}`); + const bytes = readFileSync(sourcePath); + const tracked = git("ls-files", "--", "xbox/live/oskiewar.js") !== ""; + const sourceStatus = git("status", "--porcelain", "--", "xbox/live/oskiewar.js"); + const dirty = sourceStatus !== ""; + const workingChanges = git("status", "--porcelain", "--", "apple/oskiewar", + "xbox/live", "xbox/native-bios", "xbox/package") + .split("\n").filter(Boolean).map((line) => line.replace(/^.{1,2}\s+/, "")); + const baseline = previous?.desired?.commit || `${git("rev-parse", "HEAD")}^`; + let committedChanges = []; + try { + committedChanges = git("diff", "--name-only", `${baseline}..HEAD`, "--", + "apple/oskiewar", "xbox/live", "xbox/native-bios", "xbox/package") + .split("\n").filter(Boolean); + } catch {} + const changed = [...new Set([...committedChanges, ...workingChanges])]; + return { hash: sha256(bytes), tracked, dirty, changed, + commit: git("rev-parse", "HEAD"), severity: classifySeverity(changed) }; +} + +async function verifyWeb(hash) { + const response = await fetch(`https://oskiewar.com/oskiewar.js?parity=${Date.now()}`, + { cache: "no-store" }); + if (!response.ok) throw new Error(`web returned HTTP ${response.status}`); + const actual = sha256(Buffer.from(await response.arrayBuffer())); + if (actual !== hash) throw new Error(`web hash ${actual.slice(0, 12)} != ${hash.slice(0, 12)}`); +} + +async function reconcile(receipt, { dryRun = false } = {}) { + const hash = receipt.desired.hash; + if (receipt.channels.web.status !== "current") { + if (dryRun) console.log("would deploy web"); + else try { + run("fish", ["lith/deploy.fish"]); + await verifyWeb(hash); + mark(receipt, "web", "current", "verified production bytes"); + } catch (error) { mark(receipt, "web", "failed", error.message); } + } + // iOS game code is the production web channel; its bundled copy remains the + // offline fallback. A native-shell change is deliberately not called live. + if (receipt.channels.web.status === "current" && + receipt.desired.severity !== "ios-native" && + receipt.desired.severity !== "multi-native") + mark(receipt, "ios", "current", "production web source; bundled fallback retained"); + else if (!dryRun) mark(receipt, "ios", "pending", + receipt.desired.severity.includes("native") ? "native iOS refresh required" : "waiting for web"); + + if (receipt.channels.xbox.status !== "current") { + if (dryRun) console.log("would deploy Xbox live source"); + else try { + run("node", ["xbox/tools/live.mjs", "deploy", "xbox/live/oskiewar.js"]); + mark(receipt, "xbox", "current", "Device Portal accepted source and launch"); + } catch (error) { mark(receipt, "xbox", "failed", error.message); } + } + return receipt; +} + +function print(receipt, current = null) { + console.log(JSON.stringify({ current, receipt, parity: receipt + ? channels.every((name) => receipt.channels[name]?.status === "current") + : false }, null, 2)); +} + +async function main() { + const [command = "status", ...args] = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + const previous = readReceipt(); + const current = sourceState(previous); + if (command === "status") return print(readReceipt(), current); + if (command === "deploy") { + if (!current.tracked || current.dirty) + throw new Error("Oskiewar source must be tracked and committed before a unified release"); + const receipt = newRelease(current.hash, current.commit, current.severity, previous); + save(receipt); + await reconcile(receipt, { dryRun }); + return print(receipt, current); + } + if (command === "deploy-xbox-dev") { + const receipt = newRelease(current.hash, current.commit, + current.severity, previous); + receipt.desired.development = true; + save(receipt); + try { + run("node", ["xbox/tools/live.mjs", "deploy", "xbox/live/oskiewar.js"]); + mark(receipt, "xbox", "current", "explicit uncommitted Xbox development release"); + } catch (error) { + mark(receipt, "xbox", "failed", error.message); + throw error; + } + mark(receipt, "web", "pending", "working source is not committed"); + mark(receipt, "ios", "pending", "waiting for unified production release"); + return print(receipt, current); + } + if (command === "reconcile") { + const receipt = readReceipt(); + if (!receipt) throw new Error("no pending Oskiewar release receipt"); + if (receipt.desired.hash !== current.hash) + throw new Error("working source differs from the pending release; deploy the new release first"); + await reconcile(receipt, { dryRun }); + return print(receipt, current); + } + throw new Error("commands: status | deploy [--dry-run] | deploy-xbox-dev | reconcile [--dry-run]"); +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) + main().catch((error) => { console.error(error.message); process.exitCode = 1; }); diff --git a/xbox/tools/tests/oskiewar-release.test.mjs b/xbox/tools/tests/oskiewar-release.test.mjs new file mode 100644 index 0000000000..9c60c821a4 --- /dev/null +++ b/xbox/tools/tests/oskiewar-release.test.mjs @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { classifySeverity, newRelease, sha256 } from "../oskiewar-release.mjs"; + +test("release fingerprint is stable", () => { + assert.equal(sha256("oskiewar"), sha256(Buffer.from("oskiewar"))); +}); + +test("native changes escalate the release", () => { + assert.equal(classifySeverity(["xbox/live/oskiewar.js"]), "live"); + assert.equal(classifySeverity(["apple/oskiewar/Sources/App.swift"]), "ios-native"); + assert.equal(classifySeverity(["xbox/native-bios/App.cpp"]), "xbox-native"); + assert.equal(classifySeverity(["apple/oskiewar/a", "xbox/native-bios/b"]), "multi-native"); +}); + +test("a new fingerprint leaves every channel with a durable obligation", () => { + const receipt = newRelease("next", "commit", "live", { + channels: { web: { hash: "old" }, ios: { hash: "next" }, xbox: { hash: "old" } }, + }); + assert.equal(receipt.channels.web.status, "pending"); + assert.equal(receipt.channels.ios.status, "current"); + assert.equal(receipt.channels.xbox.status, "pending"); +});