SteelAssaultGame.ts 72 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049
  1. import {
  2. _decorator,
  3. AudioClip,
  4. AudioSource,
  5. Camera,
  6. Canvas,
  7. Color,
  8. Component,
  9. EventKeyboard,
  10. EventMouse,
  11. EventTouch,
  12. Graphics,
  13. ImageAsset,
  14. input,
  15. Input,
  16. KeyCode,
  17. Label,
  18. Layers,
  19. Node,
  20. resources,
  21. ResolutionPolicy,
  22. Sprite,
  23. SpriteFrame,
  24. UIOpacity,
  25. UITransform,
  26. Vec3,
  27. view
  28. } from 'cc';
  29. import { HP_PANEL_DATA_URI, LEVEL1_BACKGROUND_DATA_URI } from './generatedLevelArtV2';
  30. const { ccclass, property } = _decorator;
  31. type WeaponId = 'rifle' | 'flame' | 'rail';
  32. type EnemyType = 'spore' | 'wing' | 'turret' | 'boss';
  33. type AudioId = 'bgm_level1_loop' | 'shot_rifle' | 'shot_flame' | 'shot_rail' | 'jump' | 'switch_weapon' | 'boss_alert';
  34. type MobileAction = 'left' | 'right' | 'up' | 'fire' | 'switch';
  35. interface Enemy {
  36. type: EnemyType;
  37. x: number;
  38. y: number;
  39. hp: number;
  40. maxHp: number;
  41. vx: number;
  42. cd: number;
  43. }
  44. interface Obstacle {
  45. x: number;
  46. y: number;
  47. width: number;
  48. height: number;
  49. }
  50. interface UiRect {
  51. x: number;
  52. y: number;
  53. width: number;
  54. height: number;
  55. }
  56. interface Bullet {
  57. x: number;
  58. y: number;
  59. vx: number;
  60. vy: number;
  61. life: number;
  62. damage: number;
  63. player: boolean;
  64. pierce: boolean;
  65. kind: WeaponId | 'enemy';
  66. homing?: boolean;
  67. hitEnemies?: Set<Enemy>;
  68. }
  69. const W = 1280;
  70. const H = 720;
  71. const SCALE = H / 540;
  72. const U = (value: number) => Math.round(value * SCALE);
  73. const US = (value: number) => value * SCALE;
  74. const GROUND = U(-174);
  75. const LANE_Y = GROUND + U(62);
  76. const BOSS_Y = LANE_Y + U(76);
  77. const END_X = U(3320);
  78. const ACTOR_RENDER_Y_OFFSET = 0;
  79. const VISUAL_LANE_Y = LANE_Y + ACTOR_RENDER_Y_OFFSET;
  80. const ENEMY_BULLET_HIT_RADIUS = U(24);
  81. const BOSS_MAX_HP = 300;
  82. const BOSS_PHASE_TWO_RATIO = 0.6;
  83. const BOSS_ENRAGE_RATIO = 0.2;
  84. const WEAPONS = {
  85. rifle: { name: 'RIFLE', cd: 0.11, damage: 4, speed: U(880) },
  86. flame: { name: 'FLAME', cd: 0.055, damage: 1.4, speed: U(520) },
  87. rail: { name: 'RAIL', cd: 0.58, damage: 5, speed: U(1250) }
  88. };
  89. function enemyVisualProjectileOffset(type: EnemyType) {
  90. if (type === 'boss') return U(18);
  91. return U(8);
  92. }
  93. @ccclass('SteelAssaultGame')
  94. export class SteelAssaultGame extends Component {
  95. @property
  96. autoStart = true;
  97. private cameraNode = new Node('RuntimeCamera');
  98. private backgroundLayer = new Node('RuntimeBackground');
  99. private renderNode = new Node('RuntimeGraphics');
  100. private spriteLayer = new Node('RuntimeSprites');
  101. private propLayer = new Node('RuntimeProps');
  102. private uiArtLayer = new Node('RuntimeUIArt');
  103. private outcomeButtonLayer = new Node('RuntimeOutcomeButtons');
  104. private hudNode = new Node('RuntimeHUD');
  105. private graphics!: Graphics;
  106. private outcomeButtonGraphics!: Graphics;
  107. private hud!: Label;
  108. private hpHud!: Label;
  109. private scoreHud!: Label;
  110. private weaponHud!: Label;
  111. private outcomeTitle!: Label;
  112. private outcomeStats!: Label;
  113. private outcomeAction!: Label;
  114. private outcomeNextAction!: Label;
  115. private outcomeRestartAction!: Label;
  116. private keys = new Set<KeyCode>();
  117. private weapon: WeaponId = 'rifle';
  118. private player = { x: U(-360), y: LANE_Y, vx: 0, vy: 0, hp: 6, face: 1, grounded: true };
  119. private previousPlayerY = LANE_Y;
  120. private enemies: Enemy[] = [];
  121. private bullets: Bullet[] = [];
  122. private spawned = new Set<number>();
  123. private boss: Enemy | null = null;
  124. private fireCd = 0;
  125. private mercy = 0;
  126. private cameraX = 0;
  127. private score = 0;
  128. private state: 'playing' | 'win' | 'next' | 'lose' = 'playing';
  129. private bossTime = 0;
  130. private elapsed = 0;
  131. private spritesReady = false;
  132. private heroSprite: Sprite | null = null;
  133. private enemySprites = new Map<Enemy, Sprite>();
  134. private bulletSprites = new Map<Bullet, Sprite>();
  135. private obstacleSprites = new Map<Obstacle, Sprite>();
  136. private heroFrames: SpriteFrame[] = [];
  137. private sporeFrames: SpriteFrame[] = [];
  138. private wingFrames: SpriteFrame[] = [];
  139. private turretFrames: SpriteFrame[] = [];
  140. private bossFrames: SpriteFrame[] = [];
  141. private projectileFrames: SpriteFrame[] = [];
  142. private sceneFrames = new Map<string, SpriteFrame>();
  143. private uiFrames = new Map<string, SpriteFrame>();
  144. private backgroundSprite: Sprite | null = null;
  145. private backgroundSprites: Sprite[] = [];
  146. private failOverlaySprite: Sprite | null = null;
  147. private outcomePanelSprite: Sprite | null = null;
  148. private hpPanelSprite: Sprite | null = null;
  149. private weaponSprite: Sprite | null = null;
  150. private heroWeaponSprite: Sprite | null = null;
  151. private weaponSlotSprites = new Map<WeaponId, Sprite>();
  152. private touchLeftSprite: Sprite | null = null;
  153. private touchRightSprite: Sprite | null = null;
  154. private touchJumpSprite: Sprite | null = null;
  155. private touchFireSprite: Sprite | null = null;
  156. private touchSwitchSprite: Sprite | null = null;
  157. private retrySprite: Sprite | null = null;
  158. private hpSprites: Sprite[] = [];
  159. private propSprites: Array<{ sprite: Sprite; worldX: number; y: number; scale: number }> = [];
  160. private createdProps = new Set<string>();
  161. private weaponSwitchTimer = 0;
  162. private musicSource!: AudioSource;
  163. private sfxSource!: AudioSource;
  164. private audioClips = new Map<AudioId, AudioClip>();
  165. private audioUnlocked = false;
  166. private musicStarted = false;
  167. private mobileLeft = false;
  168. private mobileRight = false;
  169. private mobileUp = false;
  170. private mobileDown = false;
  171. private mobileFire = false;
  172. private joystickTilt = { x: 0, y: 0 };
  173. private pointerActions = new Map<number, MobileAction>();
  174. private switchInputLock = 0;
  175. private viewportWidth = W;
  176. private sceneReady = false;
  177. private waves = [
  178. { x: U(420) },
  179. { x: U(860) },
  180. { x: U(1280) },
  181. { x: U(1680) },
  182. { x: U(2200) },
  183. { x: U(2700) }
  184. ];
  185. private obstacles: Obstacle[] = [
  186. { x: U(520), y: LANE_Y, width: U(118), height: U(58) },
  187. { x: U(1120), y: LANE_Y, width: U(138), height: U(72) },
  188. { x: U(2220), y: LANE_Y, width: U(132), height: U(64) },
  189. { x: U(2600), y: LANE_Y + U(8), width: U(132), height: U(58) },
  190. { x: U(2760), y: LANE_Y + U(78), width: U(126), height: U(56) },
  191. { x: U(2940), y: LANE_Y + U(48), width: U(132), height: U(58) },
  192. { x: U(3100), y: LANE_Y + U(132), width: U(138), height: U(56) },
  193. { x: U(3190), y: LANE_Y + U(82), width: U(132), height: U(72) }
  194. ];
  195. onLoad() {
  196. console.log('[SteelAssaultGame] boot');
  197. this.bootstrapScene();
  198. input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
  199. input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
  200. input.on(Input.EventType.TOUCH_START, this.onTouchStart, this);
  201. input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
  202. input.on(Input.EventType.TOUCH_END, this.onTouchEnd, this);
  203. input.on(Input.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
  204. input.on(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
  205. input.on(Input.EventType.MOUSE_MOVE, this.onMouseMove, this);
  206. input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this);
  207. }
  208. onDestroy() {
  209. view.setResizeCallback(null);
  210. input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
  211. input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
  212. input.off(Input.EventType.TOUCH_START, this.onTouchStart, this);
  213. input.off(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
  214. input.off(Input.EventType.TOUCH_END, this.onTouchEnd, this);
  215. input.off(Input.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
  216. input.off(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
  217. input.off(Input.EventType.MOUSE_MOVE, this.onMouseMove, this);
  218. input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this);
  219. }
  220. update(dt: number) {
  221. this.elapsed += dt;
  222. if (this.state === 'playing') {
  223. this.updatePlayer(dt);
  224. this.updateObstacleHits();
  225. this.updateWaves();
  226. this.updateEnemies(dt);
  227. this.updateBullets(dt);
  228. this.weaponSwitchTimer = Math.max(0, this.weaponSwitchTimer - dt);
  229. this.switchInputLock = Math.max(0, this.switchInputLock - dt);
  230. this.cameraX = Math.max(0, Math.min(END_X - 600, this.player.x - 120));
  231. }
  232. this.draw();
  233. }
  234. private bootstrapScene() {
  235. this.hideBootCanvas();
  236. this.node.layer = Layers.Enum.UI_2D;
  237. const transform = this.node.getComponent(UITransform) ?? this.node.addComponent(UITransform);
  238. transform.setContentSize(this.viewportWidth, H);
  239. const canvas = this.node.getComponent(Canvas) ?? this.node.addComponent(Canvas);
  240. canvas.alignCanvasWithScreen = true;
  241. this.node.addChild(this.cameraNode);
  242. this.cameraNode.layer = Layers.Enum.UI_2D;
  243. this.cameraNode.setPosition(0, 0, 1000);
  244. const camera = this.cameraNode.addComponent(Camera);
  245. //camera.orthoHeight = H / 2;
  246. camera.visibility = Layers.Enum.UI_2D;
  247. camera.clearColor = new Color(7, 12, 24, 255);
  248. canvas.cameraComponent = camera;
  249. const musicNode = new Node('RuntimeMusic');
  250. musicNode.layer = Layers.Enum.UI_2D;
  251. this.node.addChild(musicNode);
  252. this.musicSource = musicNode.addComponent(AudioSource);
  253. this.musicSource.loop = true;
  254. this.musicSource.volume = 0.22;
  255. const sfxNode = new Node('RuntimeSfx');
  256. sfxNode.layer = Layers.Enum.UI_2D;
  257. this.node.addChild(sfxNode);
  258. this.sfxSource = sfxNode.addComponent(AudioSource);
  259. this.sfxSource.volume = 0.9;
  260. this.node.addChild(this.backgroundLayer);
  261. this.backgroundLayer.layer = Layers.Enum.UI_2D;
  262. this.backgroundLayer.addComponent(UITransform).setContentSize(this.viewportWidth, H);
  263. this.node.addChild(this.renderNode);
  264. this.renderNode.layer = Layers.Enum.UI_2D;
  265. this.renderNode.addComponent(UITransform).setContentSize(this.viewportWidth, H);
  266. this.graphics = this.renderNode.addComponent(Graphics);
  267. this.node.addChild(this.propLayer);
  268. this.propLayer.layer = Layers.Enum.UI_2D;
  269. this.propLayer.addComponent(UITransform).setContentSize(this.viewportWidth, H);
  270. this.node.addChild(this.spriteLayer);
  271. this.spriteLayer.layer = Layers.Enum.UI_2D;
  272. this.spriteLayer.addComponent(UITransform).setContentSize(this.viewportWidth, H);
  273. this.node.addChild(this.uiArtLayer);
  274. this.uiArtLayer.layer = Layers.Enum.UI_2D;
  275. this.uiArtLayer.addComponent(UITransform).setContentSize(this.viewportWidth, H);
  276. this.node.addChild(this.outcomeButtonLayer);
  277. this.outcomeButtonLayer.layer = Layers.Enum.UI_2D;
  278. this.outcomeButtonLayer.addComponent(UITransform).setContentSize(this.viewportWidth, H);
  279. this.outcomeButtonGraphics = this.outcomeButtonLayer.addComponent(Graphics);
  280. this.bindOutcomeRestartNode(this.outcomeButtonLayer);
  281. this.node.addChild(this.hudNode);
  282. this.hudNode.layer = Layers.Enum.UI_2D;
  283. this.hudNode.addComponent(UITransform).setContentSize(W, U(64));
  284. this.hudNode.setPosition(0, H / 2 - U(34), 0);
  285. this.hud = this.hudNode.addComponent(Label);
  286. this.hud.fontSize = 22;
  287. this.hud.lineHeight = 28;
  288. this.hud.color = new Color(230, 250, 255, 255);
  289. this.hud.string = 'STEEL ASSAULT';
  290. const half = this.visibleWorldWidth() / 2;
  291. const hpNode = new Node('HpHud');
  292. hpNode.layer = Layers.Enum.UI_2D;
  293. this.node.addChild(hpNode);
  294. hpNode.addComponent(UITransform).setContentSize(U(210), U(52));
  295. hpNode.setPosition(-half + U(112), H / 2 - U(30), 8);
  296. this.hpHud = hpNode.addComponent(Label);
  297. this.hpHud.fontSize = 30;
  298. this.hpHud.lineHeight = 34;
  299. this.hpHud.color = new Color(255, 255, 255, 255);
  300. const scoreNode = new Node('ScoreHud');
  301. scoreNode.layer = Layers.Enum.UI_2D;
  302. this.node.addChild(scoreNode);
  303. scoreNode.addComponent(UITransform).setContentSize(U(300), U(52));
  304. scoreNode.setPosition(half - U(126), H / 2 - U(30), 8);
  305. this.scoreHud = scoreNode.addComponent(Label);
  306. this.scoreHud.fontSize = 30;
  307. this.scoreHud.lineHeight = 34;
  308. this.scoreHud.color = new Color(255, 244, 165, 255);
  309. const weaponNode = new Node('WeaponHud');
  310. weaponNode.layer = Layers.Enum.UI_2D;
  311. this.node.addChild(weaponNode);
  312. weaponNode.addComponent(UITransform).setContentSize(U(276), U(28));
  313. weaponNode.setPosition(half - U(126), H / 2 - U(60), 24);
  314. this.weaponHud = weaponNode.addComponent(Label);
  315. this.weaponHud.fontSize = 16;
  316. this.weaponHud.lineHeight = 20;
  317. this.weaponHud.color = new Color(230, 255, 255, 255);
  318. const outcomeTitleNode = new Node('OutcomeTitle');
  319. outcomeTitleNode.layer = Layers.Enum.UI_2D;
  320. this.node.addChild(outcomeTitleNode);
  321. outcomeTitleNode.addComponent(UITransform).setContentSize(U(720), U(72));
  322. outcomeTitleNode.setPosition(0, U(70), 28);
  323. this.outcomeTitle = outcomeTitleNode.addComponent(Label);
  324. this.outcomeTitle.fontSize = 46;
  325. this.outcomeTitle.lineHeight = 54;
  326. this.outcomeTitle.color = new Color(255, 244, 168, 255);
  327. const outcomeStatsNode = new Node('OutcomeStats');
  328. outcomeStatsNode.layer = Layers.Enum.UI_2D;
  329. this.node.addChild(outcomeStatsNode);
  330. outcomeStatsNode.addComponent(UITransform).setContentSize(U(720), U(56));
  331. outcomeStatsNode.setPosition(0, U(10), 28);
  332. this.outcomeStats = outcomeStatsNode.addComponent(Label);
  333. this.outcomeStats.fontSize = 28;
  334. this.outcomeStats.lineHeight = 34;
  335. this.outcomeStats.color = new Color(225, 250, 255, 255);
  336. const outcomeActionNode = new Node('OutcomeAction');
  337. outcomeActionNode.layer = Layers.Enum.UI_2D;
  338. this.node.addChild(outcomeActionNode);
  339. outcomeActionNode.addComponent(UITransform).setContentSize(U(620), U(90));
  340. outcomeActionNode.setPosition(0, U(-92), 28);
  341. this.outcomeAction = outcomeActionNode.addComponent(Label);
  342. this.outcomeAction.fontSize = 28;
  343. this.outcomeAction.lineHeight = 36;
  344. this.outcomeAction.color = new Color(14, 29, 36, 255);
  345. const outcomeNextNode = new Node('OutcomeNextAction');
  346. outcomeNextNode.layer = Layers.Enum.UI_2D;
  347. this.node.addChild(outcomeNextNode);
  348. outcomeNextNode.addComponent(UITransform).setContentSize(U(220), U(64));
  349. outcomeNextNode.setPosition(U(-126), U(-100), 30);
  350. this.outcomeNextAction = outcomeNextNode.addComponent(Label);
  351. this.outcomeNextAction.fontSize = 26;
  352. this.outcomeNextAction.lineHeight = 32;
  353. this.outcomeNextAction.color = new Color(23, 32, 38, 255);
  354. const outcomeRestartNode = new Node('OutcomeRestartAction');
  355. outcomeRestartNode.layer = Layers.Enum.UI_2D;
  356. this.node.addChild(outcomeRestartNode);
  357. outcomeRestartNode.addComponent(UITransform).setContentSize(U(220), U(64));
  358. outcomeRestartNode.setPosition(U(126), U(-100), 30);
  359. this.outcomeRestartAction = outcomeRestartNode.addComponent(Label);
  360. this.outcomeRestartAction.fontSize = 26;
  361. this.outcomeRestartAction.lineHeight = 32;
  362. this.outcomeRestartAction.color = new Color(235, 252, 255, 255);
  363. view.resizeWithBrowserSize(true);
  364. this.applyFullscreenLandscapeLayout();
  365. view.setResizeCallback(() => {
  366. this.applyFullscreenLandscapeLayout();
  367. this.draw();
  368. });
  369. this.loadSpriteSheets();
  370. this.loadSceneUiArt();
  371. this.loadAudioAssets();
  372. this.sceneReady = true;
  373. this.draw();
  374. }
  375. private hideBootCanvas() {
  376. const bootCanvas = this.node.scene?.getChildByName('BootCanvas');
  377. if (bootCanvas) bootCanvas.active = false;
  378. }
  379. private applyFullscreenLandscapeLayout() {
  380. // 横屏游戏标准方案:固定高度,宽度按屏幕比例自动扩展
  381. view.setDesignResolutionSize(W, H, ResolutionPolicy.FIXED_HEIGHT);
  382. const visible = view.getVisibleSize();
  383. const worldWidth = Math.max(W, visible.width || W);
  384. this.viewportWidth = worldWidth;
  385. const camera = this.cameraNode.getComponent(Camera);
  386. if (camera) {
  387. camera.orthoHeight = H / 2;
  388. }
  389. for (const node of [
  390. this.node,
  391. this.backgroundLayer,
  392. this.renderNode,
  393. this.propLayer,
  394. this.spriteLayer,
  395. this.uiArtLayer,
  396. this.hudNode
  397. ]) {
  398. const transform = node.getComponent(UITransform);
  399. if (transform) {
  400. transform.setContentSize(worldWidth, H);
  401. }
  402. }
  403. this.resizeFullscreenBackground();
  404. console.log(
  405. '[SteelAssaultGame] layout',
  406. 'frame:', view.getFrameSize(),
  407. 'visible:', view.getVisibleSize(),
  408. 'viewport:', view.getViewportRect(),
  409. 'worldWidth:', this.viewportWidth
  410. );
  411. }
  412. private resizeFullscreenBackground() {
  413. if (!this.backgroundSprite) return;
  414. if (!this.backgroundSprite) return;
  415. const width = this.visibleWorldWidth();
  416. this.backgroundSprite.sizeMode = Sprite.SizeMode.CUSTOM;
  417. const transform = this.backgroundSprite.node.getComponent(UITransform);
  418. if (transform) {
  419. transform.setContentSize(width, H);
  420. }
  421. this.backgroundSprite.node.setPosition(0, 0, 0);
  422. this.backgroundSprite.node.setScale(new Vec3(1, 1, 1));
  423. }
  424. private loadSpriteSheets() {
  425. const hero = [
  426. 'hero_idle',
  427. 'hero_walk_1',
  428. 'hero_walk_2',
  429. 'hero_walk_3',
  430. 'hero_walk_4',
  431. 'hero_jump_rise',
  432. 'hero_jump_fall'
  433. ];
  434. const enemies = [
  435. 'spore_idle',
  436. 'spore_walk_1',
  437. 'spore_walk_2',
  438. 'wing_idle',
  439. 'wing_flap_1',
  440. 'wing_flap_2',
  441. 'turret_idle',
  442. 'turret_aim',
  443. 'turret_fire'
  444. ];
  445. const boss = [
  446. 'boss_idle_closed',
  447. 'boss_idle_pulse',
  448. 'boss_attack_charge',
  449. 'boss_core_enraged'
  450. ];
  451. const projectiles = [
  452. 'rifle_1',
  453. 'rifle_2',
  454. 'rifle_3',
  455. 'rifle_4',
  456. 'flame_1',
  457. 'flame_2',
  458. 'flame_3',
  459. 'flame_4',
  460. 'rail_1',
  461. 'rail_2',
  462. 'acid_1',
  463. 'acid_2',
  464. 'acid_3',
  465. 'acid_4'
  466. ];
  467. this.heroFrames = new Array<SpriteFrame>(hero.length);
  468. const enemyFrames = new Array<SpriteFrame>(enemies.length);
  469. this.bossFrames = new Array<SpriteFrame>(boss.length);
  470. this.projectileFrames = new Array<SpriteFrame>(projectiles.length);
  471. const jobs: Array<{ name: string; frames: SpriteFrame[]; index: number }> = [];
  472. hero.forEach((name, index) => jobs.push({ name, frames: this.heroFrames, index }));
  473. enemies.forEach((name, index) => jobs.push({ name, frames: enemyFrames, index }));
  474. boss.forEach((name, index) => jobs.push({ name, frames: this.bossFrames, index }));
  475. projectiles.forEach((name, index) => jobs.push({ name, frames: this.projectileFrames, index }));
  476. let pending = jobs.length;
  477. let failed = false;
  478. const done = () => {
  479. pending -= 1;
  480. if (pending === 0 && !failed) {
  481. this.sporeFrames = enemyFrames.slice(0, 3);
  482. this.wingFrames = enemyFrames.slice(3, 6);
  483. this.turretFrames = enemyFrames.slice(6, 9);
  484. this.spritesReady = true;
  485. console.log('[SteelAssaultGame] imagegen frame assets ready');
  486. }
  487. };
  488. for (const job of jobs) {
  489. resources.load(`sprites/imagegen/frames/${job.name}`, ImageAsset, (err, image) => {
  490. if (err || !image) {
  491. failed = true;
  492. console.warn(`[SteelAssaultGame] sprite frame fallback for ${job.name}`, err);
  493. done();
  494. return;
  495. }
  496. job.frames[job.index] = SpriteFrame.createWithImage(image);
  497. done();
  498. });
  499. }
  500. }
  501. private loadSceneUiArt() {
  502. this.loadDataUriFrame(LEVEL1_BACKGROUND_DATA_URI, (frame) => {
  503. this.sceneFrames.set('background', frame);
  504. this.backgroundSprites = [];
  505. const sprite = this.createSprite('Level1Background', this.backgroundLayer);
  506. sprite.spriteFrame = frame;
  507. sprite.sizeMode = Sprite.SizeMode.CUSTOM;
  508. sprite.node.getComponent(UITransform)?.setContentSize(this.visibleWorldWidth(), H);
  509. sprite.node.setPosition(0, 0, 0);
  510. sprite.node.setScale(new Vec3(1, 1, 1));
  511. const opacity = sprite.node.getComponent(UIOpacity) ?? sprite.node.addComponent(UIOpacity);
  512. opacity.opacity = 255;
  513. this.backgroundSprite = sprite;
  514. this.backgroundSprites.push(sprite);
  515. this.resizeFullscreenBackground();
  516. });
  517. this.loadDataUriFrame(HP_PANEL_DATA_URI, (frame) => {
  518. this.uiFrames.set('hp_numeric_plaque_v2', frame);
  519. this.hpPanelSprite = this.createSprite('HpNumericPanel', this.uiArtLayer);
  520. this.hpPanelSprite.spriteFrame = frame;
  521. this.hpPanelSprite.node.setPosition(-this.visibleWorldWidth() / 2 + 112, H / 2 - U(30), 6);
  522. this.hpPanelSprite.node.setScale(new Vec3(US(0.5), US(0.5), 1));
  523. });
  524. const ui = [
  525. 'weapon_rifle',
  526. 'weapon_flame',
  527. 'weapon_rail',
  528. 'retry_button',
  529. 'outcome_fail_panel',
  530. 'outcome_win_panel',
  531. 'mobile_left',
  532. 'mobile_right',
  533. 'mobile_jump',
  534. 'mobile_fire',
  535. 'mobile_switch'
  536. ];
  537. for (const name of ui) {
  538. this.loadFrame(`sprites/imagegen/ui_scene/ui/${name}`, (frame) => this.uiFrames.set(name, frame));
  539. }
  540. this.loadFrame('sprites/imagegen/frames/obstacle_block', (frame) => this.uiFrames.set('obstacle_block', frame));
  541. }
  542. private loadFrame(path: string, use: (frame: SpriteFrame) => void) {
  543. resources.load(path, ImageAsset, (err, image) => {
  544. if (err || !image) {
  545. console.warn(`[SteelAssaultGame] art fallback for ${path}`, err);
  546. return;
  547. }
  548. use(SpriteFrame.createWithImage(image));
  549. });
  550. }
  551. private loadDataUriFrame(dataUri: string, use: (frame: SpriteFrame) => void) {
  552. const image = new Image();
  553. image.onload = () => {
  554. use(SpriteFrame.createWithImage(image));
  555. };
  556. image.onerror = () => {
  557. console.warn('[SteelAssaultGame] embedded image asset failed');
  558. };
  559. image.src = dataUri;
  560. }
  561. private loadAudioAssets() {
  562. const ids: AudioId[] = [
  563. 'bgm_level1_loop',
  564. 'shot_rifle',
  565. 'shot_flame',
  566. 'shot_rail',
  567. 'jump',
  568. 'switch_weapon',
  569. 'boss_alert'
  570. ];
  571. for (const id of ids) {
  572. resources.load(`audio/${id}`, AudioClip, (err, clip) => {
  573. if (err || !clip) {
  574. console.warn(`[SteelAssaultGame] audio fallback for ${id}`, err);
  575. return;
  576. }
  577. this.audioClips.set(id, clip);
  578. if (id === 'bgm_level1_loop' && this.audioUnlocked) this.startMusic();
  579. });
  580. }
  581. }
  582. private unlockAudio() {
  583. this.audioUnlocked = true;
  584. this.startMusic();
  585. }
  586. private startMusic() {
  587. if (this.musicStarted || !this.musicSource) return;
  588. const clip = this.audioClips.get('bgm_level1_loop');
  589. if (!clip) return;
  590. this.musicSource.clip = clip;
  591. this.musicSource.loop = true;
  592. this.musicSource.volume = 0.22;
  593. this.musicSource.play();
  594. this.musicStarted = true;
  595. }
  596. private playSfx(id: AudioId, volume = 0.85) {
  597. const clip = this.audioClips.get(id);
  598. if (!clip || !this.sfxSource) return;
  599. this.sfxSource.playOneShot(clip, volume);
  600. }
  601. private updatePlayer(dt: number) {
  602. this.previousPlayerY = this.player.y;
  603. const left = this.keys.has(KeyCode.KEY_A) || this.keys.has(KeyCode.ARROW_LEFT);
  604. const right = this.keys.has(KeyCode.KEY_D) || this.keys.has(KeyCode.ARROW_RIGHT);
  605. const jump = this.keys.has(KeyCode.SPACE) || this.keys.has(KeyCode.KEY_W) || this.keys.has(KeyCode.ARROW_UP);
  606. const down = this.keys.has(KeyCode.KEY_S) || this.keys.has(KeyCode.ARROW_DOWN) || this.mobileDown;
  607. const moveLeft = left || this.mobileLeft;
  608. const moveRight = right || this.mobileRight;
  609. const fire = this.keys.has(KeyCode.KEY_J) || this.keys.has(KeyCode.ENTER) || this.mobileFire;
  610. this.player.vx = moveLeft === moveRight ? 0 : moveLeft ? -U(270) : U(270);
  611. if (this.player.vx !== 0) this.player.face = this.player.vx > 0 ? 1 : -1;
  612. if (jump || this.mobileUp) this.tryJump();
  613. if (down && !this.player.grounded) this.player.vy -= U(1700) * dt;
  614. this.player.vy -= U(1450) * dt;
  615. this.player.x = Math.max(U(-410), Math.min(END_X - U(180), this.player.x + this.player.vx * dt));
  616. this.player.y += this.player.vy * dt;
  617. if (this.player.y <= LANE_Y) {
  618. this.player.y = LANE_Y;
  619. this.player.vy = 0;
  620. this.player.grounded = true;
  621. }
  622. this.fireCd -= dt;
  623. this.mercy = Math.max(0, this.mercy - dt);
  624. if (fire) this.tryFire();
  625. if (this.player.x > U(3090) && !this.boss) this.spawnBoss();
  626. }
  627. private updateWaves() {
  628. for (let index = 0; index < this.waves.length; index++) {
  629. const wave = this.waves[index];
  630. const activeNormalEnemies = this.enemies.filter((enemy) => enemy.type !== 'boss').length;
  631. if (!this.spawned.has(index) && this.player.x > wave.x - U(520) && activeNormalEnemies === 0) {
  632. this.spawned.add(index);
  633. this.spawnEnemy(this.randomEnemyType(index), wave.x + U(120), index);
  634. }
  635. }
  636. }
  637. private randomEnemyType(_seed: number): Exclude<EnemyType, 'boss'> {
  638. return 'spore';
  639. }
  640. private updateObstacleHits() {
  641. const playerHalfWidth = U(22);
  642. const landingTolerance = U(14);
  643. for (const obstacle of this.obstacles) {
  644. const left = obstacle.x - obstacle.width / 2;
  645. const right = obstacle.x + obstacle.width / 2;
  646. const top = obstacle.y + obstacle.height;
  647. const horizontalOverlap = this.player.x + playerHalfWidth > left && this.player.x - playerHalfWidth < right;
  648. if (!horizontalOverlap) continue;
  649. const wasNearOrAboveTop = this.previousPlayerY >= top - landingTolerance;
  650. const crossedOrRestingOnTop = this.player.y <= top + landingTolerance && this.player.y >= top - landingTolerance;
  651. const fallingOntoTop = this.player.vy <= 0 && wasNearOrAboveTop && crossedOrRestingOnTop;
  652. const alreadyOnTop = this.player.grounded && Math.abs(this.player.y - top) <= landingTolerance;
  653. if (fallingOntoTop || alreadyOnTop) {
  654. this.player.y = top;
  655. this.player.vy = 0;
  656. this.player.grounded = true;
  657. return;
  658. }
  659. const nearObstacleBody = this.player.y < top - U(10) && this.player.y > obstacle.y - U(4);
  660. if (nearObstacleBody) {
  661. const pushLeft = Math.abs(this.player.x - left);
  662. const pushRight = Math.abs(this.player.x - right);
  663. this.player.x = pushLeft < pushRight ? left - playerHalfWidth : right + playerHalfWidth;
  664. this.player.vx = 0;
  665. return;
  666. }
  667. }
  668. }
  669. private updateEnemies(dt: number) {
  670. for (const enemy of this.enemies) {
  671. enemy.cd -= dt;
  672. if (enemy.type === 'spore') {
  673. enemy.y = LANE_Y;
  674. enemy.vx = enemy.x > this.player.x ? -U(82) : U(82);
  675. }
  676. if (enemy.type === 'wing') {
  677. enemy.vx = enemy.x > this.player.x ? -U(70) : U(70);
  678. enemy.y = LANE_Y + Math.sin(this.elapsed * 3 + enemy.x * 0.02) * U(4);
  679. if (enemy.cd <= 0 && this.canEnemyShoot(enemy, U(360))) {
  680. this.enemyShot(enemy.x, enemy.y + this.enemyShotOffset(enemy), this.player.x > enemy.x ? 1 : -1, U(130));
  681. enemy.cd = 3.4;
  682. }
  683. }
  684. if (enemy.type === 'turret') {
  685. enemy.y = LANE_Y;
  686. enemy.vx = 0;
  687. if (enemy.cd <= 0 && this.canEnemyShoot(enemy, U(420))) {
  688. this.enemyShot(enemy.x - U(20), enemy.y + this.enemyShotOffset(enemy) + U(8), this.player.x > enemy.x ? 1 : -1, U(135));
  689. enemy.cd = 3.1;
  690. }
  691. }
  692. if (enemy.type === 'boss') {
  693. this.bossTime += dt;
  694. enemy.vx = Math.sin(this.bossTime * 0.7) * U(28);
  695. enemy.y = BOSS_Y + Math.sin(this.bossTime * 1.1) * U(10);
  696. const hpRatio = enemy.hp / enemy.maxHp;
  697. const phase = hpRatio <= BOSS_ENRAGE_RATIO ? 3 : hpRatio <= BOSS_PHASE_TWO_RATIO ? 2 : 1;
  698. if (enemy.cd <= 0 && this.canEnemyShoot(enemy, U(980))) {
  699. const shotX = enemy.x - U(115);
  700. const targetX = this.player.x;
  701. const targetY = this.player.y + U(12);
  702. this.enemyShotAt(shotX, enemy.y + this.enemyShotOffset(enemy) + U(46), targetX, targetY, phase >= 3 ? U(255) : U(220), 2.6);
  703. if (phase >= 2) this.enemyShotAt(shotX, enemy.y + this.enemyShotOffset(enemy) - U(18), targetX, targetY + U(34), U(205), 2.45);
  704. if (phase >= 3) this.enemyShotAt(shotX, enemy.y + this.enemyShotOffset(enemy) + U(88), targetX, targetY - U(28), U(190), 2.35);
  705. if (phase >= 3 && this.enemies.filter((e) => e.type === 'spore').length < 5) this.spawnEnemy('spore', enemy.x - U(180), 0);
  706. enemy.cd = phase === 1 ? 1.65 : phase === 2 ? 1.28 : 0.95;
  707. }
  708. }
  709. enemy.x += enemy.vx * dt;
  710. if (this.enemyTouchesPlayer(enemy)) this.damagePlayer();
  711. }
  712. this.enemies = this.enemies.filter((enemy) => {
  713. if (enemy.hp > 0) return true;
  714. this.score += enemy.type === 'boss' ? 5000 : 100;
  715. if (enemy.type === 'boss') this.state = 'win';
  716. return false;
  717. });
  718. }
  719. private updateBullets(dt: number) {
  720. for (const bullet of this.bullets) {
  721. bullet.life -= dt;
  722. if (!bullet.player && bullet.homing) {
  723. const speed = Math.max(1, Math.sqrt(bullet.vx * bullet.vx + bullet.vy * bullet.vy));
  724. const dx = this.player.x - bullet.x;
  725. const dy = this.player.y + U(12) - bullet.y;
  726. const length = Math.max(1, Math.sqrt(dx * dx + dy * dy));
  727. const blend = Math.min(1, dt * 1.45);
  728. bullet.vx = bullet.vx * (1 - blend) + (dx / length) * speed * blend;
  729. bullet.vy = bullet.vy * (1 - blend) + (dy / length) * speed * blend;
  730. }
  731. bullet.x += bullet.vx * dt;
  732. bullet.y += bullet.vy * dt;
  733. if (bullet.player) {
  734. for (const enemy of this.enemies) {
  735. const radius = enemy.type === 'boss' ? U(155) : U(44);
  736. if (bullet.pierce && bullet.hitEnemies?.has(enemy)) continue;
  737. if (enemy.hp > 0 && this.hit(bullet.x, bullet.y, enemy.x, enemy.y, radius)) {
  738. enemy.hp -= bullet.damage;
  739. if (bullet.pierce) {
  740. if (!bullet.hitEnemies) bullet.hitEnemies = new Set<Enemy>();
  741. bullet.hitEnemies.add(enemy);
  742. } else {
  743. bullet.life = 0;
  744. }
  745. }
  746. }
  747. } else if (this.hit(bullet.x, bullet.y, this.player.x, this.player.y, ENEMY_BULLET_HIT_RADIUS)) {
  748. bullet.life = 0;
  749. this.damagePlayer();
  750. }
  751. }
  752. this.bullets = this.bullets.filter((bullet) => bullet.life > 0 && Math.abs(bullet.x - this.player.x) < U(1320));
  753. }
  754. private fire() {
  755. const spec = WEAPONS[this.weapon];
  756. this.fireCd = spec.cd;
  757. this.playSfx(this.weapon === 'flame' ? 'shot_flame' : this.weapon === 'rail' ? 'shot_rail' : 'shot_rifle', this.weapon === 'rail' ? 0.86 : 0.74);
  758. if (this.weapon === 'flame') {
  759. for (let i = 0; i < 2; i++) {
  760. this.bullets.push({
  761. x: this.player.x + this.player.face * U(46),
  762. y: this.player.y + U(12),
  763. vx: this.player.face * spec.speed,
  764. vy: i ? U(70) : -U(40),
  765. life: 0.24,
  766. damage: spec.damage,
  767. player: true,
  768. pierce: false,
  769. kind: 'flame'
  770. });
  771. }
  772. return;
  773. }
  774. this.bullets.push({
  775. x: this.player.x + this.player.face * U(46),
  776. y: this.player.y + U(12),
  777. vx: this.player.face * spec.speed,
  778. vy: 0,
  779. life: 0.85,
  780. damage: spec.damage,
  781. player: true,
  782. pierce: this.weapon === 'rail',
  783. kind: this.weapon
  784. });
  785. }
  786. private enemyShot(x: number, y: number, dir: number, speed = U(155), life = 1.35) {
  787. this.bullets.push({ x, y, vx: dir * speed, vy: 0, life, damage: 1, player: false, pierce: false, kind: 'enemy' });
  788. }
  789. private enemyShotAt(x: number, y: number, targetX: number, targetY: number, speed: number, life = 2.4) {
  790. const dx = targetX - x;
  791. const dy = targetY - y;
  792. const length = Math.max(1, Math.sqrt(dx * dx + dy * dy));
  793. this.bullets.push({
  794. x,
  795. y,
  796. vx: (dx / length) * speed,
  797. vy: (dy / length) * speed,
  798. life,
  799. damage: 1,
  800. player: false,
  801. pierce: false,
  802. kind: 'enemy',
  803. homing: false
  804. });
  805. }
  806. private spawnEnemy(type: Exclude<EnemyType, 'boss'>, x: number, seed: number) {
  807. const hp = type === 'turret' ? 24 : type === 'wing' ? 16 : 12;
  808. this.enemies.push({
  809. type,
  810. x,
  811. y: LANE_Y,
  812. hp,
  813. maxHp: hp,
  814. vx: 0,
  815. cd: 1.25 + seed * 0.28
  816. });
  817. }
  818. private spawnBoss() {
  819. const boss = { type: 'boss' as const, x: U(3290), y: BOSS_Y, hp: BOSS_MAX_HP, maxHp: BOSS_MAX_HP, vx: 0, cd: 0.5 };
  820. this.boss = boss;
  821. this.enemies.push(boss);
  822. this.playSfx('boss_alert', 0.95);
  823. }
  824. private damagePlayer() {
  825. if (this.mercy > 0) return;
  826. this.player.hp = Math.max(0, this.player.hp - 1);
  827. this.mercy = 0.55;
  828. if (this.player.hp <= 0) {
  829. this.state = 'lose';
  830. this.clearMobileControls();
  831. }
  832. }
  833. private draw() {
  834. if (!this.sceneReady || !this.graphics) return;
  835. const g = this.graphics;
  836. g.clear();
  837. if (!this.backgroundSprite) {
  838. const half = this.visibleWorldWidth() / 2;
  839. g.fillColor = new Color(8, 18, 37, 255);
  840. g.fillRect(-half, -H / 2, half * 2, H);
  841. }
  842. this.drawWorld(g);
  843. this.drawHudBackplates(g);
  844. this.syncEnvironmentProps();
  845. this.drawObstacleSolids(g);
  846. this.drawActors(g);
  847. this.drawControlButtons(g);
  848. this.drawOutcomePanel(g);
  849. this.syncUiArt();
  850. this.drawHud();
  851. }
  852. private drawHudBackplates(g: Graphics) {
  853. if (this.state === 'lose' || this.state === 'win' || this.state === 'next') {
  854. g.fillColor = new Color(0, 0, 0, 145);
  855. const half = this.visibleWorldWidth() / 2;
  856. g.fillRect(-half, -H / 2, half * 2, H);
  857. return;
  858. }
  859. const half = this.visibleWorldWidth() / 2;
  860. const hpX = -half + 18;
  861. const scorePanel = this.scoreWeaponPanelRect();
  862. g.fillColor = new Color(5, 12, 22, 205);
  863. g.fillRect(hpX, H / 2 - U(54), 232, 56);
  864. g.fillRect(scorePanel.x, scorePanel.y, scorePanel.width, scorePanel.height);
  865. g.fillColor = new Color(255, 207, 72, 230);
  866. g.fillRect(hpX, H / 2 - U(54), 4, 56);
  867. g.fillRect(scorePanel.x, scorePanel.y, 4, scorePanel.height);
  868. g.strokeColor = new Color(64, 238, 255, 190);
  869. g.lineWidth = 2;
  870. g.rect(hpX, H / 2 - U(54), 232, 56);
  871. g.rect(scorePanel.x, scorePanel.y, scorePanel.width, scorePanel.height);
  872. g.stroke();
  873. }
  874. private drawWorld(g: Graphics) {
  875. const offset = this.cameraX;
  876. const half = this.visibleWorldWidth() / 2;
  877. if (this.backgroundSprite) return;
  878. if (!this.backgroundSprite) {
  879. g.fillColor = new Color(18, 52, 50, 255);
  880. g.fillRect(-half, GROUND - 38, half * 2, 110);
  881. for (let i = 0; i < 46; i++) {
  882. const x = i * 120 - offset - 520;
  883. g.fillColor = i % 2 ? new Color(33, 112, 78, 190) : new Color(39, 92, 104, 170);
  884. g.circle(x, -84 + (i % 4) * 10, 58 + (i % 3) * 9);
  885. g.fill();
  886. }
  887. g.fillColor = new Color(90, 240, 255, 150);
  888. for (let i = 0; i < 18; i++) {
  889. const x = i * 235 - offset - 430;
  890. g.moveTo(x, GROUND + 10);
  891. g.lineTo(x + 30, GROUND + 88);
  892. g.lineTo(x + 62, GROUND + 10);
  893. g.close();
  894. g.fill();
  895. }
  896. }
  897. g.fillColor = new Color(42, 112, 70, 255);
  898. g.fillRect(-half, GROUND - 70, half * 2, 44);
  899. g.fillColor = new Color(105, 224, 141, 255);
  900. for (let x = -half; x < half; x += 24) {
  901. g.moveTo(x, GROUND - 28);
  902. g.lineTo(x + 9, GROUND + 4);
  903. g.lineTo(x + 18, GROUND - 28);
  904. g.close();
  905. g.fill();
  906. }
  907. }
  908. private syncEnvironmentProps() {
  909. this.hideEnvironmentProps();
  910. for (const prop of this.propSprites) {
  911. prop.sprite.node.setPosition(prop.worldX - this.cameraX, prop.y, 1);
  912. prop.sprite.node.active = false;
  913. }
  914. if (this.backgroundSprite) {
  915. const maxDrift = 0;
  916. const drift = -Math.min(maxDrift, (this.cameraX / Math.max(1, END_X - 600)) * maxDrift);
  917. for (let i = 0; i < this.backgroundSprites.length; i++) {
  918. this.backgroundSprites[i].node.setPosition(drift, 0, 0);
  919. }
  920. }
  921. }
  922. private hideEnvironmentProps() {
  923. for (const sprite of this.propSprites) sprite.sprite.node.active = false;
  924. }
  925. private drawObstacleSolids(g: Graphics) {
  926. if (this.state !== 'playing') {
  927. this.hideObstacleSprites();
  928. return;
  929. }
  930. const frame = this.uiFrames.get('obstacle_block');
  931. if (frame) {
  932. this.syncObstacleSprites(frame);
  933. return;
  934. }
  935. this.hideObstacleSprites();
  936. for (const obstacle of this.obstacles) {
  937. const x = obstacle.x - this.cameraX;
  938. const half = this.visibleWorldWidth() / 2;
  939. if (x < -half - 180 || x > half + 180) continue;
  940. const y = obstacle.y + ACTOR_RENDER_Y_OFFSET;
  941. const left = x - obstacle.width / 2;
  942. g.fillColor = new Color(15, 28, 32, 235);
  943. g.fillRect(left, y, obstacle.width, obstacle.height);
  944. g.strokeColor = new Color(255, 212, 66, 255);
  945. g.lineWidth = 4;
  946. g.rect(left, y, obstacle.width, obstacle.height);
  947. g.stroke();
  948. g.fillColor = new Color(255, 73, 44, 235);
  949. for (let stripe = left + 8; stripe < left + obstacle.width - 8; stripe += 26) {
  950. g.moveTo(stripe, y + 8);
  951. g.lineTo(stripe + 14, y + 8);
  952. g.lineTo(stripe + 4, y + obstacle.height - 8);
  953. g.lineTo(stripe - 10, y + obstacle.height - 8);
  954. g.close();
  955. g.fill();
  956. }
  957. g.fillColor = new Color(64, 238, 255, 245);
  958. g.fillRect(left + 8, y + obstacle.height - 8, obstacle.width - 16, 5);
  959. }
  960. }
  961. private syncObstacleSprites(frame: SpriteFrame) {
  962. const half = this.visibleWorldWidth() / 2;
  963. for (const obstacle of this.obstacles) {
  964. let sprite = this.obstacleSprites.get(obstacle);
  965. if (!sprite) {
  966. sprite = this.createSprite('ObstacleBlock', this.propLayer);
  967. sprite.sizeMode = Sprite.SizeMode.CUSTOM;
  968. this.obstacleSprites.set(obstacle, sprite);
  969. }
  970. const x = obstacle.x - this.cameraX;
  971. const visible = x >= -half - U(180) && x <= half + U(180);
  972. sprite.spriteFrame = frame;
  973. sprite.node.getComponent(UITransform)?.setContentSize(obstacle.width, obstacle.height);
  974. sprite.node.setPosition(x, obstacle.y + obstacle.height / 2 + ACTOR_RENDER_Y_OFFSET, 2);
  975. sprite.node.active = visible && this.state === 'playing';
  976. }
  977. }
  978. private hideObstacleSprites() {
  979. for (const sprite of this.obstacleSprites.values()) sprite.node.active = false;
  980. }
  981. private drawControlButtons(g: Graphics) {
  982. if (this.state !== 'playing') return;
  983. const panel = this.scoreWeaponPanelRect();
  984. const slotWidth = U(54);
  985. const slotHeight = U(36);
  986. const y = panel.y + U(18);
  987. (Object.keys(WEAPONS) as WeaponId[]).forEach((id, index) => {
  988. const sx = panel.x + U(68) + index * U(64) - slotWidth / 2;
  989. const active = this.weapon === id;
  990. g.fillColor = active ? this.weaponColor(id, 235) : new Color(17, 34, 42, 225);
  991. g.fillRect(sx, y, slotWidth, slotHeight);
  992. g.strokeColor = active ? new Color(255, 255, 255, 245) : new Color(64, 238, 255, 120);
  993. g.lineWidth = active ? 4 : 2;
  994. g.rect(sx, y, slotWidth, slotHeight);
  995. g.stroke();
  996. });
  997. }
  998. private drawActors(g: Graphics) {
  999. if (this.isOutcomeState()) {
  1000. this.hideActorSprites();
  1001. return;
  1002. }
  1003. if (this.spritesReady) {
  1004. this.drawSpriteActors(g);
  1005. return;
  1006. }
  1007. const ox = this.cameraX;
  1008. const px = this.player.x - ox;
  1009. const py = this.player.y + ACTOR_RENDER_Y_OFFSET;
  1010. g.fillColor = this.mercy > 0 ? new Color(255, 255, 255, 220) : new Color(38, 56, 74, 255);
  1011. g.fillRect(px - 22, py - 36, 44, 62);
  1012. g.fill();
  1013. g.fillColor = new Color(70, 217, 255, 255);
  1014. g.fillRect(px - 16, py + 17, 32, 24);
  1015. g.fill();
  1016. g.fillColor = new Color(247, 209, 106, 255);
  1017. g.fillRect(px + this.player.face * 6, py - 8, this.player.face * 42, 10);
  1018. for (const enemy of this.enemies) {
  1019. const x = enemy.x - ox;
  1020. const y = this.enemyRenderY(enemy);
  1021. if (enemy.type === 'spore') {
  1022. g.fillColor = new Color(124, 48, 164, 255);
  1023. g.circle(x, y, 30);
  1024. g.fill();
  1025. g.fillColor = new Color(168, 255, 92, 255);
  1026. g.circle(x - 9, y + 8, 5);
  1027. g.fill();
  1028. g.circle(x + 11, y + 6, 5);
  1029. g.fill();
  1030. } else if (enemy.type === 'wing') {
  1031. g.fillColor = new Color(80, 244, 255, U(130));
  1032. g.moveTo(x - 45, y + 4);
  1033. g.lineTo(x - 10, y + 28);
  1034. g.lineTo(x - 12, y - 18);
  1035. g.close();
  1036. g.fill();
  1037. g.moveTo(x + 45, y + 4);
  1038. g.lineTo(x + 10, y + 28);
  1039. g.lineTo(x + 12, y - 18);
  1040. g.close();
  1041. g.fill();
  1042. g.fillColor = new Color(122, 51, 200, 255);
  1043. g.circle(x, y, 22);
  1044. g.fill();
  1045. } else if (enemy.type === 'turret') {
  1046. g.fillColor = new Color(36, 62, 81, 255);
  1047. g.fillRect(x - 38, y - 22, 76, 42);
  1048. g.fillColor = new Color(146, 251, 255, 255);
  1049. g.fillRect(x - 52, y + 4, 48, 10);
  1050. } else {
  1051. const ratio = Math.max(0, enemy.hp / enemy.maxHp);
  1052. g.fillColor = new Color(58, 28, 100, 255);
  1053. g.circle(x, y, 104);
  1054. g.fill();
  1055. g.fillColor = new Color(255, 79, 174, 255);
  1056. g.circle(x, y + 8, 56);
  1057. g.fill();
  1058. g.fillColor = new Color(249, 255, 144, 255);
  1059. g.circle(x, y + 16, 20);
  1060. g.fill();
  1061. g.fillColor = new Color(255, 79, 174, 255);
  1062. g.fillRect(-140, H / 2 - U(70), 280 * ratio, 10);
  1063. }
  1064. }
  1065. for (const bullet of this.bullets) {
  1066. g.fillColor = bullet.player
  1067. ? this.weaponColor(bullet.kind as WeaponId)
  1068. : new Color(185, 84, 255, 255);
  1069. const x = bullet.x - ox;
  1070. const y = bullet.y + ACTOR_RENDER_Y_OFFSET;
  1071. if (bullet.pierce) g.fillRect(x - 32, y - 4, 64, 8);
  1072. else {
  1073. g.circle(x, y, bullet.player ? 8 : 11);
  1074. g.fill();
  1075. }
  1076. }
  1077. }
  1078. private drawSpriteActors(g: Graphics) {
  1079. this.syncHeroSprite();
  1080. this.syncEnemySprites();
  1081. this.syncBulletSprites();
  1082. if (this.boss && this.boss.hp > 0) {
  1083. const ratio = Math.max(0, this.boss.hp / this.boss.maxHp);
  1084. g.fillColor = new Color(255, 79, 174, 255);
  1085. g.fillRect(-140, H / 2 - U(70), 280 * ratio, 10);
  1086. }
  1087. }
  1088. private hideActorSprites() {
  1089. if (this.heroSprite) this.heroSprite.node.active = false;
  1090. if (this.heroWeaponSprite) this.heroWeaponSprite.node.active = false;
  1091. for (const sprite of this.enemySprites.values()) sprite.node.active = false;
  1092. for (const sprite of this.bulletSprites.values()) sprite.node.active = false;
  1093. }
  1094. private syncHeroSprite() {
  1095. if (!this.heroSprite) this.heroSprite = this.createSprite('HeroSprite');
  1096. const frame = this.getHeroFrame();
  1097. if (frame) this.heroSprite.spriteFrame = frame;
  1098. const x = this.player.x - this.cameraX;
  1099. const blink = this.mercy > 0 && Math.floor(this.elapsed * 18) % 2 === 0;
  1100. const switchLift = this.weaponSwitchTimer > 0 ? Math.sin((this.weaponSwitchTimer / 0.22) * Math.PI) : 0;
  1101. this.heroSprite.node.active = !blink;
  1102. this.heroSprite.node.setPosition(x, this.player.y + ACTOR_RENDER_Y_OFFSET + switchLift * U(5), 0);
  1103. this.heroSprite.node.setScale(new Vec3((US(0.53) + switchLift * US(0.035)) * this.player.face, US(0.53) - switchLift * US(0.014), 1));
  1104. this.syncHeroWeaponSprite(x, switchLift, blink);
  1105. }
  1106. private syncHeroWeaponSprite(heroX: number, switchLift: number, blink: boolean) {
  1107. const frame = this.uiFrames.get(`weapon_${this.weapon}`);
  1108. if (!frame) {
  1109. if (this.heroWeaponSprite) this.heroWeaponSprite.node.active = false;
  1110. return;
  1111. }
  1112. if (!this.heroWeaponSprite) this.heroWeaponSprite = this.createSprite('HeroWeaponSprite');
  1113. this.heroWeaponSprite.spriteFrame = frame;
  1114. const recoil = this.weaponSwitchTimer > 0 ? Math.sin((this.weaponSwitchTimer / 0.22) * Math.PI) * U(12) : 0;
  1115. const x = heroX + this.player.face * (U(34) - recoil);
  1116. const y = this.player.y + ACTOR_RENDER_Y_OFFSET + 8 + switchLift * U(18);
  1117. this.heroWeaponSprite.node.setPosition(x, y, 1);
  1118. this.heroWeaponSprite.node.setScale(new Vec3(US(0.22) * this.player.face, US(0.22), 1));
  1119. this.heroWeaponSprite.node.active = this.state === 'playing' && !blink;
  1120. }
  1121. private syncEnemySprites() {
  1122. for (const [enemy, sprite] of this.enemySprites) {
  1123. if (!this.enemies.includes(enemy)) {
  1124. sprite.node.destroy();
  1125. this.enemySprites.delete(enemy);
  1126. }
  1127. }
  1128. for (const enemy of this.enemies) {
  1129. let sprite = this.enemySprites.get(enemy);
  1130. if (!sprite) {
  1131. sprite = this.createSprite(`Enemy_${enemy.type}`);
  1132. this.enemySprites.set(enemy, sprite);
  1133. }
  1134. const frame = this.getEnemyFrame(enemy);
  1135. if (frame) sprite.spriteFrame = frame;
  1136. sprite.node.setPosition(enemy.x - this.cameraX, this.enemyRenderY(enemy), 0);
  1137. const face = enemy.type === 'spore'
  1138. ? (enemy.x > this.player.x ? 1 : -1)
  1139. : (enemy.x > this.player.x ? -1 : 1);
  1140. const scale = enemy.type === 'boss' ? US(1.06) : enemy.type === 'turret' ? US(0.44) : enemy.type === 'wing' ? US(0.41) : US(0.51);
  1141. sprite.node.setScale(new Vec3(scale * face, scale, 1));
  1142. sprite.node.active = true;
  1143. }
  1144. }
  1145. private syncBulletSprites() {
  1146. for (const [bullet, sprite] of this.bulletSprites) {
  1147. if (!this.bullets.includes(bullet)) {
  1148. sprite.node.destroy();
  1149. this.bulletSprites.delete(bullet);
  1150. }
  1151. }
  1152. for (const bullet of this.bullets) {
  1153. let sprite = this.bulletSprites.get(bullet);
  1154. if (!sprite) {
  1155. sprite = this.createSprite('Projectile');
  1156. this.bulletSprites.set(bullet, sprite);
  1157. }
  1158. const frame = this.getProjectileFrame(bullet);
  1159. if (frame) sprite.spriteFrame = frame;
  1160. const scale = bullet.kind === 'rail' ? US(0.27) : bullet.kind === 'flame' ? US(0.24) : US(0.22);
  1161. const face = bullet.vx >= 0 ? 1 : -1;
  1162. sprite.node.setPosition(bullet.x - this.cameraX, bullet.y + ACTOR_RENDER_Y_OFFSET, 0);
  1163. sprite.node.setScale(new Vec3(scale * face, scale, 1));
  1164. sprite.node.active = true;
  1165. }
  1166. }
  1167. private createSprite(name: string, parent: Node = this.spriteLayer) {
  1168. const node = new Node(name);
  1169. node.layer = Layers.Enum.UI_2D;
  1170. parent.addChild(node);
  1171. node.addComponent(UITransform);
  1172. const sprite = node.addComponent(Sprite);
  1173. sprite.type = Sprite.Type.SIMPLE;
  1174. sprite.sizeMode = Sprite.SizeMode.RAW;
  1175. return sprite;
  1176. }
  1177. private syncUiArt() {
  1178. this.syncResponsiveUiLayout();
  1179. this.syncHpArt();
  1180. this.syncWeaponArt();
  1181. this.syncTouchControls();
  1182. this.syncOutcomePanelArt();
  1183. this.syncOutcomeButtons();
  1184. this.syncRetryArt();
  1185. this.syncOutcomeText();
  1186. if (this.failOverlaySprite) this.failOverlaySprite.node.active = this.state === 'lose';
  1187. }
  1188. private syncResponsiveUiLayout() {
  1189. const half = this.visibleWorldWidth() / 2;
  1190. const scorePanel = this.scoreWeaponPanelRect();
  1191. this.hpHud.node.setPosition(-half + U(112), H / 2 - U(30), 8);
  1192. this.scoreHud.node.setPosition(scorePanel.x + scorePanel.width / 2, scorePanel.y + scorePanel.height - U(34), 8);
  1193. this.weaponHud.node.setPosition(scorePanel.x + scorePanel.width / 2, scorePanel.y + U(70), 24);
  1194. if (this.hpPanelSprite) this.hpPanelSprite.node.setPosition(-half + U(112), H / 2 - U(30), 6);
  1195. }
  1196. private scoreWeaponPanelRect(): UiRect {
  1197. const half = this.visibleWorldWidth() / 2;
  1198. return {
  1199. x: half - U(430),
  1200. y: H / 2 - U(150),
  1201. width: U(306),
  1202. height: U(112)
  1203. };
  1204. }
  1205. private syncTouchControls() {
  1206. const visible = this.state === 'playing';
  1207. this.touchLeftSprite = this.syncMobileButton(
  1208. this.touchLeftSprite,
  1209. 'TouchLeft',
  1210. this.uiFrames.get('mobile_left'),
  1211. this.leftButtonCenter(),
  1212. this.mobileLeft,
  1213. visible,
  1214. () => this.applyMobileAction('left', true),
  1215. () => this.applyMobileAction('left', false)
  1216. );
  1217. this.touchRightSprite = this.syncMobileButton(
  1218. this.touchRightSprite,
  1219. 'TouchRight',
  1220. this.uiFrames.get('mobile_right'),
  1221. this.rightButtonCenter(),
  1222. this.mobileRight,
  1223. visible,
  1224. () => this.applyMobileAction('right', true),
  1225. () => this.applyMobileAction('right', false)
  1226. );
  1227. this.touchJumpSprite = this.syncMobileButton(
  1228. this.touchJumpSprite,
  1229. 'TouchJump',
  1230. this.uiFrames.get('mobile_jump'),
  1231. this.jumpButtonCenter(),
  1232. this.mobileUp,
  1233. visible,
  1234. () => this.applyMobileAction('up', true),
  1235. () => this.applyMobileAction('up', false),
  1236. false
  1237. );
  1238. const fireFrame = this.uiFrames.get('mobile_fire');
  1239. if (fireFrame) {
  1240. if (!this.touchFireSprite) {
  1241. this.touchFireSprite = this.createSprite('TouchFire', this.uiArtLayer);
  1242. this.bindTouchControlNode(
  1243. this.touchFireSprite.node,
  1244. () => {
  1245. this.mobileFire = true;
  1246. },
  1247. () => {
  1248. this.mobileFire = false;
  1249. }
  1250. );
  1251. }
  1252. const pos = this.fireButtonCenter();
  1253. this.touchFireSprite.spriteFrame = fireFrame;
  1254. this.touchFireSprite.node.getComponent(UITransform)?.setContentSize(U(128), U(128));
  1255. this.touchFireSprite.node.setPosition(pos.x, pos.y, 18);
  1256. this.touchFireSprite.node.setScale(new Vec3(this.mobileFire ? 1.08 : 1, this.mobileFire ? 1.08 : 1, 1));
  1257. this.touchFireSprite.node.active = visible;
  1258. this.setSpriteOpacity(this.touchFireSprite, this.mobileFire ? 245 : 202);
  1259. }
  1260. const switchFrame = this.uiFrames.get('mobile_switch');
  1261. if (switchFrame) {
  1262. if (!this.touchSwitchSprite) {
  1263. this.touchSwitchSprite = this.createSprite('TouchSwitch', this.uiArtLayer);
  1264. this.bindTouchControlNode(
  1265. this.touchSwitchSprite.node,
  1266. () => this.tryCycleWeapon(),
  1267. () => {},
  1268. false
  1269. );
  1270. }
  1271. const pos = this.switchButtonCenter();
  1272. const active = this.weaponSwitchTimer > 0;
  1273. this.touchSwitchSprite.spriteFrame = switchFrame;
  1274. this.touchSwitchSprite.node.getComponent(UITransform)?.setContentSize(U(112), U(112));
  1275. this.touchSwitchSprite.node.setPosition(pos.x, pos.y, 18);
  1276. this.touchSwitchSprite.node.setScale(new Vec3(active ? 1.08 : 1, active ? 1.08 : 1, 1));
  1277. this.touchSwitchSprite.node.active = visible;
  1278. this.setSpriteOpacity(this.touchSwitchSprite, active ? 245 : 202);
  1279. }
  1280. }
  1281. private syncMobileButton(
  1282. sprite: Sprite | null,
  1283. name: string,
  1284. frame: SpriteFrame | undefined,
  1285. pos: { x: number; y: number },
  1286. active: boolean,
  1287. visible: boolean,
  1288. onPress: () => void,
  1289. onRelease: () => void,
  1290. repeatOnMove = true
  1291. ) {
  1292. if (!frame) return sprite;
  1293. if (!sprite) {
  1294. sprite = this.createSprite(name, this.uiArtLayer);
  1295. sprite.sizeMode = Sprite.SizeMode.CUSTOM;
  1296. this.bindTouchControlNode(sprite.node, onPress, onRelease, repeatOnMove);
  1297. }
  1298. sprite.spriteFrame = frame;
  1299. sprite.node.getComponent(UITransform)?.setContentSize(U(116), U(116));
  1300. sprite.node.setPosition(pos.x, pos.y, 18);
  1301. sprite.node.setScale(new Vec3(active ? 1.08 : 1, active ? 1.08 : 1, 1));
  1302. sprite.node.active = visible;
  1303. this.setSpriteOpacity(sprite, active ? 245 : 202);
  1304. return sprite;
  1305. }
  1306. private bindTouchControlNode(
  1307. node: Node,
  1308. onPress: (event: EventTouch | EventMouse) => void,
  1309. onRelease: (event: EventTouch | EventMouse) => void,
  1310. repeatOnMove = true
  1311. ) {
  1312. const bindState = node as unknown as { __steelTouchBound?: boolean };
  1313. if (bindState.__steelTouchBound) return;
  1314. bindState.__steelTouchBound = true;
  1315. const press = (event: EventTouch | EventMouse) => {
  1316. if (this.state !== 'playing') return;
  1317. this.unlockAudio();
  1318. onPress(event);
  1319. this.stopTouchEvent(event);
  1320. };
  1321. const release = (event: EventTouch | EventMouse) => {
  1322. onRelease(event);
  1323. this.stopTouchEvent(event);
  1324. };
  1325. node.on(Node.EventType.TOUCH_START, press, this);
  1326. if (repeatOnMove) node.on(Node.EventType.TOUCH_MOVE, press, this);
  1327. node.on(Node.EventType.TOUCH_END, release, this);
  1328. node.on(Node.EventType.TOUCH_CANCEL, release, this);
  1329. node.on(Node.EventType.MOUSE_DOWN, press, this);
  1330. if (repeatOnMove) node.on(Node.EventType.MOUSE_MOVE, press, this);
  1331. node.on(Node.EventType.MOUSE_UP, release, this);
  1332. node.on(Node.EventType.MOUSE_LEAVE, release, this);
  1333. }
  1334. private bindOutcomeRestartNode(node: Node) {
  1335. const bindState = node as unknown as { __steelOutcomeBound?: boolean };
  1336. if (bindState.__steelOutcomeBound) return;
  1337. bindState.__steelOutcomeBound = true;
  1338. const restart = (event: EventTouch | EventMouse) => {
  1339. if (!this.isOutcomeState()) return;
  1340. this.unlockAudio();
  1341. if (this.handleOutcomeAction(event)) this.stopTouchEvent(event);
  1342. };
  1343. node.on(Node.EventType.TOUCH_START, restart, this);
  1344. node.on(Node.EventType.MOUSE_DOWN, restart, this);
  1345. }
  1346. private stopTouchEvent(event: EventTouch | EventMouse) {
  1347. const stoppable = event as unknown as { stopPropagation?: () => void; propagationStopped?: boolean };
  1348. stoppable.stopPropagation?.();
  1349. stoppable.propagationStopped = true;
  1350. }
  1351. private handleOutcomeAction(event: EventTouch | EventMouse) {
  1352. const { x, y } = this.pointerToWorldPoint(event);
  1353. if (this.state === 'win') {
  1354. if (this.pointInRect(x, y, this.outcomeNextRect())) {
  1355. this.state = 'next';
  1356. this.clearMobileControls();
  1357. return true;
  1358. }
  1359. if (this.pointInRect(x, y, this.outcomeRestartRect())) {
  1360. this.reset();
  1361. return true;
  1362. }
  1363. return false;
  1364. }
  1365. if ((this.state === 'lose' || this.state === 'next') && this.pointInRect(x, y, this.outcomeSingleRestartRect())) {
  1366. this.reset();
  1367. return true;
  1368. }
  1369. return false;
  1370. }
  1371. private pointInRect(x: number, y: number, rect: UiRect) {
  1372. return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
  1373. }
  1374. private setMobileDirectionFromTouchPoint(event: EventTouch | EventMouse) {
  1375. this.clearMobileMove();
  1376. const action = this.resolveDirectionalButtonFromPoints(this.pointerWorldPointCandidates(event));
  1377. if (action) this.applyMobileAction(action, true);
  1378. }
  1379. private setSpriteOpacity(sprite: Sprite, opacity: number) {
  1380. const uiOpacity = sprite.node.getComponent(UIOpacity) ?? sprite.node.addComponent(UIOpacity);
  1381. uiOpacity.opacity = opacity;
  1382. }
  1383. private syncHpArt() {
  1384. if (this.hpPanelSprite) {
  1385. this.hpPanelSprite.node.active = this.state === 'playing';
  1386. for (const sprite of this.hpSprites) sprite.node.active = false;
  1387. this.hpHud.string = this.state === 'playing' ? `生命 ${Math.max(0, this.player.hp)}/6` : '';
  1388. return;
  1389. }
  1390. const hpFrame = this.uiFrames.get('hp_capsule');
  1391. const emptyFrame = this.uiFrames.get('hp_empty') ?? hpFrame;
  1392. if (!hpFrame) return;
  1393. while (this.hpSprites.length < 6) {
  1394. const sprite = this.createSprite(`HpCapsule_${this.hpSprites.length}`, this.uiArtLayer);
  1395. this.hpSprites.push(sprite);
  1396. }
  1397. const startX = -this.visibleWorldWidth() / 2 + U(46);
  1398. for (let i = 0; i < this.hpSprites.length; i++) {
  1399. const sprite = this.hpSprites[i];
  1400. sprite.spriteFrame = i < this.player.hp ? hpFrame : emptyFrame;
  1401. sprite.node.setPosition(startX + i * U(38), H / 2 - U(42), 5);
  1402. sprite.node.setScale(new Vec3(0.19, 0.19, 1));
  1403. sprite.node.active = this.state === 'playing';
  1404. }
  1405. }
  1406. private syncWeaponArt() {
  1407. if (this.weaponSprite) this.weaponSprite.node.active = false;
  1408. const ids = Object.keys(WEAPONS) as WeaponId[];
  1409. const panel = this.scoreWeaponPanelRect();
  1410. for (let index = 0; index < ids.length; index++) {
  1411. const id = ids[index];
  1412. const frame = this.uiFrames.get(`weapon_${id}`);
  1413. if (!frame) continue;
  1414. let sprite = this.weaponSlotSprites.get(id);
  1415. if (!sprite) {
  1416. sprite = this.createSprite(`WeaponSlot_${id}`, this.uiArtLayer);
  1417. this.weaponSlotSprites.set(id, sprite);
  1418. }
  1419. const active = id === this.weapon;
  1420. sprite.spriteFrame = frame;
  1421. sprite.node.setPosition(panel.x + U(68) + index * U(64), panel.y + U(36) + (active ? 3 : 0), 20);
  1422. sprite.node.setScale(new Vec3(active ? US(0.146) : US(0.125), active ? US(0.146) : US(0.125), 1));
  1423. sprite.node.active = this.state === 'playing';
  1424. }
  1425. }
  1426. private syncRetryArt() {
  1427. const frame = this.uiFrames.get('retry_button');
  1428. if (!frame) return;
  1429. if (!this.retrySprite) this.retrySprite = this.createSprite('RetryButton', this.uiArtLayer);
  1430. this.retrySprite.spriteFrame = frame;
  1431. this.retrySprite.node.setPosition(0, U(-118), 20);
  1432. this.retrySprite.node.setScale(new Vec3(US(0.52), US(0.52), 1));
  1433. this.retrySprite.node.active = false;
  1434. }
  1435. private syncOutcomePanelArt() {
  1436. const visible = this.isOutcomeState();
  1437. const frame = this.uiFrames.get(this.state === 'lose' ? 'outcome_fail_panel' : 'outcome_win_panel');
  1438. if (!frame) {
  1439. if (this.outcomePanelSprite) this.outcomePanelSprite.node.active = false;
  1440. return;
  1441. }
  1442. if (!this.outcomePanelSprite) {
  1443. this.outcomePanelSprite = this.createSprite('OutcomePanel', this.uiArtLayer);
  1444. this.bindOutcomeRestartNode(this.outcomePanelSprite.node);
  1445. }
  1446. this.outcomePanelSprite.spriteFrame = frame;
  1447. this.outcomePanelSprite.sizeMode = Sprite.SizeMode.CUSTOM;
  1448. this.outcomePanelSprite.node.getComponent(UITransform)?.setContentSize(U(760), U(450));
  1449. this.outcomePanelSprite.node.setPosition(0, U(-8), 20);
  1450. this.outcomePanelSprite.node.active = visible;
  1451. }
  1452. private getHeroFrame() {
  1453. if (!this.player.grounded) return this.player.vy > 0 ? this.heroFrames[5] : this.heroFrames[6];
  1454. if (Math.abs(this.player.vx) > 1) return this.heroFrames[1 + (Math.floor(this.elapsed * 10) % 4)];
  1455. return this.heroFrames[0];
  1456. }
  1457. private getEnemyFrame(enemy: Enemy) {
  1458. if (enemy.type === 'spore') return this.sporeFrames[Math.floor(this.elapsed * 7) % 3];
  1459. if (enemy.type === 'wing') return this.wingFrames[Math.floor(this.elapsed * 10) % 3];
  1460. if (enemy.type === 'turret') return this.turretFrames[enemy.cd > 0.82 ? 2 : Math.floor(this.elapsed * 2) % 2];
  1461. if (enemy.hp <= enemy.maxHp * BOSS_ENRAGE_RATIO) return this.bossFrames[3];
  1462. if (enemy.cd > 0.72) return this.bossFrames[2];
  1463. return this.bossFrames[Math.floor(this.elapsed * 2) % 2];
  1464. }
  1465. private getProjectileFrame(bullet: Bullet) {
  1466. const pulse = Math.floor(this.elapsed * 18) % 4;
  1467. if (bullet.kind === 'rifle') return this.projectileFrames[pulse];
  1468. if (bullet.kind === 'flame') return this.projectileFrames[4 + pulse];
  1469. if (bullet.kind === 'rail') return this.projectileFrames[8 + (Math.floor(this.elapsed * 12) % 2)];
  1470. return this.projectileFrames[10 + pulse];
  1471. }
  1472. private enemyVisualOffset(enemy: Enemy) {
  1473. if (enemy.type === 'boss') return 0;
  1474. return 0;
  1475. }
  1476. private enemyRenderY(enemy: Enemy) {
  1477. return enemy.y + ACTOR_RENDER_Y_OFFSET + this.enemyVisualOffset(enemy);
  1478. }
  1479. private enemyShotOffset(enemy: Enemy) {
  1480. return enemyVisualProjectileOffset(enemy.type);
  1481. }
  1482. private canEnemyShoot(enemy: Enemy, range: number) {
  1483. if (enemy.type === 'boss') return Math.abs(enemy.x - this.player.x) < range && Math.abs(enemy.y - this.player.y) < 260;
  1484. return Math.abs(enemy.x - this.player.x) < range && Math.abs(enemy.y - this.player.y) < 120;
  1485. }
  1486. private weaponColor(id: WeaponId, alpha = 255) {
  1487. if (id === 'flame') return new Color(255, 96, 42, alpha);
  1488. if (id === 'rail') return new Color(72, 236, 255, alpha);
  1489. return new Color(255, 216, 92, alpha);
  1490. }
  1491. private enemyTouchesPlayer(enemy: Enemy) {
  1492. const dx = Math.abs(this.player.x - enemy.x);
  1493. const dy = Math.abs(this.player.y - enemy.y);
  1494. if (enemy.type === 'boss') return dx < U(135) && dy < U(150);
  1495. return dx < U(34) && dy < U(44);
  1496. }
  1497. private drawOutcomePanel(g: Graphics) {
  1498. if (!this.isOutcomeState() || this.outcomePanelSprite?.node.active) return;
  1499. g.fillColor = new Color(8, 20, 28, 225);
  1500. g.fillRect(-330, -158, 660, 278);
  1501. g.strokeColor = new Color(78, 232, 255, 245);
  1502. g.lineWidth = 4;
  1503. g.rect(-330, -158, 660, 278);
  1504. g.stroke();
  1505. g.fillColor = new Color(255, 207, 72, 245);
  1506. g.fillRect(-210, -132, 420, 58);
  1507. g.fillColor = new Color(62, 238, 255, 210);
  1508. g.fillRect(-330, 110, 660, 6);
  1509. g.fillRect(-330, -158, 660, 6);
  1510. }
  1511. private syncOutcomeButtons() {
  1512. if (!this.outcomeButtonGraphics) return;
  1513. const g = this.outcomeButtonGraphics;
  1514. g.clear();
  1515. const visible = this.isOutcomeState();
  1516. this.outcomeButtonLayer.active = visible;
  1517. if (!visible) return;
  1518. if (this.state === 'win') {
  1519. this.drawOutcomeButton(g, this.outcomeNextRect(), new Color(255, 211, 88, 238), new Color(255, 248, 190, 255), new Color(24, 136, 169, 210));
  1520. this.drawOutcomeButton(g, this.outcomeRestartRect(), new Color(17, 48, 66, 238), new Color(86, 230, 255, 235), new Color(255, 185, 82, 215));
  1521. return;
  1522. }
  1523. const restartColor = this.state === 'lose'
  1524. ? new Color(116, 25, 31, 238)
  1525. : new Color(17, 48, 66, 238);
  1526. const accentColor = this.state === 'lose'
  1527. ? new Color(255, 86, 72, 235)
  1528. : new Color(255, 185, 82, 215);
  1529. this.drawOutcomeButton(g, this.outcomeSingleRestartRect(), restartColor, new Color(255, 226, 205, 235), accentColor);
  1530. }
  1531. private drawOutcomeButton(g: Graphics, rect: UiRect, fill: Color, stroke: Color, accent: Color) {
  1532. g.fillColor = fill;
  1533. g.fillRect(rect.x, rect.y, rect.width, rect.height);
  1534. g.fillColor = accent;
  1535. g.fillRect(rect.x, rect.y, U(7), rect.height);
  1536. g.fillRect(rect.x + rect.width - U(7), rect.y, U(7), rect.height);
  1537. g.strokeColor = stroke;
  1538. g.lineWidth = 3;
  1539. g.rect(rect.x, rect.y, rect.width, rect.height);
  1540. g.stroke();
  1541. }
  1542. private outcomeNextRect(): UiRect {
  1543. return { x: U(-226), y: U(-130), width: U(196), height: U(58) };
  1544. }
  1545. private outcomeRestartRect(): UiRect {
  1546. return { x: U(30), y: U(-130), width: U(196), height: U(58) };
  1547. }
  1548. private outcomeSingleRestartRect(): UiRect {
  1549. return { x: U(-118), y: U(-130), width: U(236), height: U(58) };
  1550. }
  1551. private syncOutcomeText() {
  1552. const visible = this.isOutcomeState();
  1553. const win = this.state === 'win';
  1554. this.outcomeTitle.node.active = visible;
  1555. this.outcomeStats.node.active = visible;
  1556. this.outcomeAction.node.active = visible && !win;
  1557. this.outcomeNextAction.node.active = visible && win;
  1558. this.outcomeRestartAction.node.active = visible && win;
  1559. if (!visible) return;
  1560. if (this.state === 'win') {
  1561. this.outcomeTitle.string = '通关成功';
  1562. this.outcomeStats.string = `得分 ${this.score.toString().padStart(5, '0')} 生命 ${Math.max(0, this.player.hp)}/6`;
  1563. this.outcomeAction.string = '';
  1564. this.outcomeNextAction.string = '选择下一关';
  1565. this.outcomeRestartAction.string = '重新开始';
  1566. this.outcomeNextAction.color = new Color(22, 31, 36, 255);
  1567. this.outcomeRestartAction.color = new Color(232, 252, 255, 255);
  1568. this.outcomeTitle.color = new Color(255, 232, 126, 255);
  1569. this.outcomeStats.color = new Color(225, 250, 255, 255);
  1570. this.outcomeAction.color = new Color(255, 238, 154, 255);
  1571. return;
  1572. }
  1573. if (this.state === 'lose') {
  1574. this.outcomeTitle.string = '挑战失败';
  1575. this.outcomeStats.string = `得分 ${this.score.toString().padStart(5, '0')}`;
  1576. this.outcomeAction.string = '重新开始';
  1577. this.outcomeAction.node.setPosition(0, U(-100), 28);
  1578. this.outcomeTitle.color = new Color(255, 92, 82, 255);
  1579. this.outcomeStats.color = new Color(225, 250, 255, 255);
  1580. this.outcomeAction.color = new Color(255, 226, 205, 255);
  1581. return;
  1582. }
  1583. this.outcomeTitle.string = '下一关筹备中';
  1584. this.outcomeStats.string = '请先返回第一关继续体验';
  1585. this.outcomeAction.string = '重新开始';
  1586. this.outcomeAction.node.setPosition(0, U(-100), 28);
  1587. }
  1588. private isOutcomeState() {
  1589. return this.state === 'win' || this.state === 'next' || this.state === 'lose';
  1590. }
  1591. private drawHud() {
  1592. let status = this.boss && this.boss.hp > 0 ? `BOSS ${Math.ceil(this.boss.hp)}/${this.boss.maxHp}` : '';
  1593. if (this.isOutcomeState()) status = '';
  1594. this.hud.string = status;
  1595. this.hudNode.setPosition(0, this.state === 'lose' ? -40 : H / 2 - U(34), 12);
  1596. this.hud.fontSize = this.state === 'lose' ? 30 : 28;
  1597. this.hud.lineHeight = this.state === 'lose' ? 34 : 32;
  1598. this.hud.color = this.state === 'lose' ? new Color(255, 240, 180, 255) : new Color(255, 255, 255, 255);
  1599. this.scoreHud.string = this.state === 'playing' ? `得分 ${this.score.toString().padStart(5, '0')}` : '';
  1600. this.hpHud.string = this.state === 'playing' ? `生命 ${Math.max(0, this.player.hp)}/6` : '';
  1601. this.weaponHud.string = '';
  1602. }
  1603. private hit(ax: number, ay: number, bx: number, by: number, radius: number) {
  1604. const dx = ax - bx;
  1605. const dy = ay - by;
  1606. return dx * dx + dy * dy < radius * radius;
  1607. }
  1608. private onKeyDown(event: EventKeyboard) {
  1609. this.unlockAudio();
  1610. if (event.keyCode === KeyCode.DIGIT_1) this.setWeapon('rifle');
  1611. if (event.keyCode === KeyCode.DIGIT_2) this.setWeapon('flame');
  1612. if (event.keyCode === KeyCode.DIGIT_3) this.setWeapon('rail');
  1613. if (event.keyCode === KeyCode.KEY_Q) this.tryCycleWeapon();
  1614. if (event.keyCode === KeyCode.KEY_R && this.state !== 'playing') this.reset();
  1615. this.keys.add(event.keyCode);
  1616. }
  1617. private onTouchStart(event: EventTouch) {
  1618. this.unlockAudio();
  1619. if (this.state === 'playing') {
  1620. this.handlePointerStart(this.pointerIdFromTouch(event), event);
  1621. return;
  1622. }
  1623. if (!this.isOutcomeState()) return;
  1624. if (this.handleOutcomeAction(event)) this.stopTouchEvent(event);
  1625. }
  1626. private onTouchMove(event: EventTouch) {
  1627. if (this.state !== 'playing') return;
  1628. this.handlePointerMove(this.pointerIdFromTouch(event), event);
  1629. }
  1630. private onTouchEnd(event: EventTouch) {
  1631. this.handlePointerEnd(this.pointerIdFromTouch(event));
  1632. }
  1633. private onMouseDown(event: EventMouse) {
  1634. if (event.getButton() !== EventMouse.BUTTON_LEFT) return;
  1635. if (this.state !== 'playing') return;
  1636. this.unlockAudio();
  1637. this.handlePointerStart(-1, event);
  1638. }
  1639. private onMouseMove(event: EventMouse) {
  1640. if (this.state !== 'playing') return;
  1641. this.handlePointerMove(-1, event);
  1642. }
  1643. private onMouseUp(event: EventMouse) {
  1644. this.handlePointerEnd(-1);
  1645. }
  1646. private pointerIdFromTouch(event: EventTouch) {
  1647. return event.getID() ?? 0;
  1648. }
  1649. private handlePointerStart(id: number, event: EventTouch | EventMouse) {
  1650. const action = this.resolveMobileActionFromEvent(event);
  1651. if (!action) return;
  1652. if (action === 'switch') {
  1653. this.tryCycleWeapon();
  1654. return;
  1655. }
  1656. this.pointerActions.set(id, action);
  1657. this.applyMobileAction(action, true);
  1658. }
  1659. private handlePointerMove(id: number, event: EventTouch | EventMouse) {
  1660. const current = this.pointerActions.get(id);
  1661. if (!current || current === 'fire') return;
  1662. const next = this.resolveMobileActionFromEvent(event);
  1663. if (next === 'left' || next === 'right' || next === 'up') {
  1664. this.applyMobileAction(current, false);
  1665. this.pointerActions.set(id, next);
  1666. this.applyMobileAction(next, true);
  1667. }
  1668. }
  1669. private handlePointerEnd(id: number) {
  1670. const action = this.pointerActions.get(id);
  1671. if (!action) return;
  1672. this.applyMobileAction(action, false);
  1673. this.pointerActions.delete(id);
  1674. }
  1675. private resolveMobileActionFromEvent(event: EventTouch | EventMouse): MobileAction | null {
  1676. const points = this.pointerWorldPointCandidates(event);
  1677. if (this.closestPointInCircle(points, this.fireButtonCenter(), U(64))) return 'fire';
  1678. if (this.closestPointInCircle(points, this.switchButtonCenter(), U(56))) return 'switch';
  1679. return this.resolveDirectionalButtonFromPoints(points);
  1680. }
  1681. private resolveDirectionalButtonFromPoints(points: Array<{ x: number; y: number }>): Extract<MobileAction, 'left' | 'right' | 'up'> | null {
  1682. if (this.closestPointInCircle(points, this.jumpButtonCenter(), U(64))) return 'up';
  1683. if (this.closestPointInCircle(points, this.leftButtonCenter(), U(64))) return 'left';
  1684. if (this.closestPointInCircle(points, this.rightButtonCenter(), U(64))) return 'right';
  1685. return null;
  1686. }
  1687. private pointerToWorldPoint(event: EventTouch | EventMouse) {
  1688. return this.pointerWorldPointCandidates(event)[0];
  1689. }
  1690. private pointerWorldPointCandidates(event: EventTouch | EventMouse) {
  1691. const visible = view.getVisibleSize();
  1692. const viewport = view.getViewportRect();
  1693. const frame = view.getFrameSize();
  1694. const spaces = [
  1695. { x: viewport.x, y: viewport.y, width: viewport.width, height: viewport.height },
  1696. { x: 0, y: 0, width: visible.width, height: visible.height },
  1697. { x: 0, y: 0, width: frame.width, height: frame.height }
  1698. ];
  1699. const locations = [this.eventUiLocation(event), event.getLocation()];
  1700. const candidates: Array<{ x: number; y: number }> = [];
  1701. const seen = new Set<string>();
  1702. for (const space of spaces) {
  1703. const width = Math.max(1, space.width);
  1704. const height = Math.max(1, space.height);
  1705. for (const location of locations) {
  1706. const localX = location.x - space.x;
  1707. const localY = location.y - space.y;
  1708. for (const y of [localY, height - localY]) {
  1709. const point = {
  1710. x: (localX / width - 0.5) * this.visibleWorldWidth(),
  1711. y: (y / height - 0.5) * H
  1712. };
  1713. const key = `${Math.round(point.x)}:${Math.round(point.y)}`;
  1714. if (!seen.has(key)) {
  1715. seen.add(key);
  1716. candidates.push(point);
  1717. }
  1718. }
  1719. }
  1720. }
  1721. return candidates;
  1722. }
  1723. private anyPointInCircle(points: Array<{ x: number; y: number }>, center: { x: number; y: number }, radius: number) {
  1724. return points.some((point) => this.hit(point.x, point.y, center.x, center.y, radius));
  1725. }
  1726. private closestPointInCircle(points: Array<{ x: number; y: number }>, center: { x: number; y: number }, radius: number) {
  1727. let closest: { x: number; y: number } | null = null;
  1728. let closestDistance = radius * radius;
  1729. for (const point of points) {
  1730. const dx = point.x - center.x;
  1731. const dy = point.y - center.y;
  1732. const distance = dx * dx + dy * dy;
  1733. if (distance <= closestDistance) {
  1734. closest = point;
  1735. closestDistance = distance;
  1736. }
  1737. }
  1738. return closest;
  1739. }
  1740. private touchToGamePoint(event: EventTouch) {
  1741. return this.pointerToWorldPoint(event);
  1742. }
  1743. private eventUiLocation(event: EventTouch | EventMouse) {
  1744. const uiEvent = event as unknown as { getUILocation?: () => { x: number; y: number } };
  1745. return uiEvent.getUILocation?.() ?? event.getLocation();
  1746. }
  1747. private tryJump() {
  1748. if (this.state !== 'playing' || !this.player.grounded) return;
  1749. this.player.vy = U(620);
  1750. this.player.grounded = false;
  1751. this.playSfx('jump', 0.72);
  1752. }
  1753. private tryFire() {
  1754. if (this.state !== 'playing' || this.fireCd > 0) return;
  1755. this.fire();
  1756. }
  1757. private applyMobileAction(action: MobileAction, active: boolean) {
  1758. if (action === 'left') {
  1759. this.mobileLeft = active;
  1760. if (active) this.mobileRight = false;
  1761. }
  1762. if (action === 'right') {
  1763. this.mobileRight = active;
  1764. if (active) this.mobileLeft = false;
  1765. }
  1766. if (action === 'up') {
  1767. this.mobileUp = active;
  1768. if (active) this.tryJump();
  1769. }
  1770. if (action === 'fire') {
  1771. this.mobileFire = active;
  1772. if (active) this.tryFire();
  1773. }
  1774. this.joystickTilt.x = this.mobileLeft ? -1 : this.mobileRight ? 1 : 0;
  1775. this.joystickTilt.y = this.mobileUp ? 1 : 0;
  1776. }
  1777. private visibleWorldWidth() {
  1778. const visible = view.getVisibleSize();
  1779. return Math.max(W, visible.width || this.viewportWidth || W);
  1780. }
  1781. private leftButtonCenter() {
  1782. const half = this.visibleWorldWidth() / 2;
  1783. return { x: -half + U(72), y: U(-208) };
  1784. }
  1785. private rightButtonCenter() {
  1786. const half = this.visibleWorldWidth() / 2;
  1787. return { x: -half + U(214), y: U(-208) };
  1788. }
  1789. private jumpButtonCenter() {
  1790. const half = this.visibleWorldWidth() / 2;
  1791. return { x: -half + U(143), y: U(-60) };
  1792. }
  1793. private fireButtonCenter() {
  1794. const half = this.visibleWorldWidth() / 2;
  1795. return { x: half - U(100), y: U(-138) };
  1796. }
  1797. private switchButtonCenter() {
  1798. const half = this.visibleWorldWidth() / 2;
  1799. return { x: half - U(225), y: U(-188) };
  1800. }
  1801. private clearMobileMove() {
  1802. this.mobileLeft = false;
  1803. this.mobileRight = false;
  1804. this.mobileUp = false;
  1805. this.mobileDown = false;
  1806. this.joystickTilt.x = 0;
  1807. this.joystickTilt.y = 0;
  1808. }
  1809. private clearMobileControls() {
  1810. this.pointerActions.clear();
  1811. this.clearMobileMove();
  1812. this.mobileFire = false;
  1813. }
  1814. private tryCycleWeapon() {
  1815. if (this.switchInputLock > 0) return;
  1816. this.switchInputLock = 0.18;
  1817. this.cycleWeapon();
  1818. }
  1819. private cycleWeapon() {
  1820. this.setWeapon(this.weapon === 'rifle' ? 'flame' : this.weapon === 'flame' ? 'rail' : 'rifle');
  1821. }
  1822. private setWeapon(next: WeaponId) {
  1823. if (this.weapon === next && this.weaponSwitchTimer > 0) return;
  1824. const changed = this.weapon !== next;
  1825. this.weapon = next;
  1826. this.weaponSwitchTimer = 0.22;
  1827. this.fireCd = Math.min(this.fireCd, 0.08);
  1828. if (changed) this.playSfx('switch_weapon', 0.78);
  1829. }
  1830. private onKeyUp(event: EventKeyboard) {
  1831. this.keys.delete(event.keyCode);
  1832. }
  1833. private reset() {
  1834. this.clearMobileControls();
  1835. this.weapon = 'rifle';
  1836. this.player = { x: U(-360), y: LANE_Y, vx: 0, vy: 0, hp: 6, face: 1, grounded: true };
  1837. this.enemies = [];
  1838. this.bullets = [];
  1839. this.spawned.clear();
  1840. this.boss = null;
  1841. this.fireCd = 0;
  1842. this.mercy = 0;
  1843. this.cameraX = 0;
  1844. this.score = 0;
  1845. this.state = 'playing';
  1846. this.bossTime = 0;
  1847. this.elapsed = 0;
  1848. for (const sprite of this.enemySprites.values()) sprite.node.destroy();
  1849. for (const sprite of this.bulletSprites.values()) sprite.node.destroy();
  1850. this.enemySprites.clear();
  1851. this.bulletSprites.clear();
  1852. }
  1853. }