refactor: 使用flutter对原先的web项目重构,正在进行中
This commit is contained in:
1 parent
273224f5cf
commit
18017f819a
205 files changed
+13563
No files matched your search
@@ -0,0 +1,383 @@
|
||||
// 玩家:还原 IWBT 的硬核手感 —— 松手即停、无加速、可变跳跃高度、二段跳。
|
||||
// 与 web 版 player.js 1:1 对应(逐帧积分完全一致,保证手感)。
|
||||
import 'dart:ui' show Canvas;
|
||||
import 'dart:math' show pi, max, min;
|
||||
import 'package:iwbtc/src/core/util.dart';
|
||||
import 'package:iwbtc/src/core/input.dart';
|
||||
import 'package:iwbtc/src/audio/sfx.dart';
|
||||
import 'package:iwbtc/src/game/sprites.dart';
|
||||
import 'package:iwbtc/src/game/views.dart';
|
||||
import 'package:iwbtc/src/game/level.dart' show Level, Tile, Mover, Cell, Bullet;
|
||||
|
||||
class SavePoint {
|
||||
double x, y;
|
||||
int face;
|
||||
SavePoint(this.x, this.y, this.face);
|
||||
}
|
||||
|
||||
class PHY {
|
||||
static const double GRAV = 0.25;
|
||||
static const double GRAV_FALL = 0.32;
|
||||
static const double GRAV_DJ = 0.35;
|
||||
static const double MAXFALL = 9;
|
||||
static const double JUMP = 6.6;
|
||||
static const double DJUMP = 6.7;
|
||||
static const double DJ_INERTIA = 0;
|
||||
static const double HSPD = 2.6;
|
||||
static const double WATER_GRAV = 0.16;
|
||||
static const double WATER_FALL = 2.6;
|
||||
static const double WATER_RISE = 0.42;
|
||||
static const double ICE_ACC = 0.18;
|
||||
static const double ICE_FRIC = 0.07;
|
||||
static const double SLIDE_FRIC = 0.12;
|
||||
static const double SLIDE_AIR = 0.02;
|
||||
static const double SLIDE_EASE = 0.14;
|
||||
static const double SLIDE_BRAKE = 0.45;
|
||||
static const double SLIDE_MIN = 0.35;
|
||||
static const double SLIDE_STOP = 0.05;
|
||||
static const double ICE_TOP = 1.6;
|
||||
}
|
||||
|
||||
class Player implements PlayerView {
|
||||
Level level;
|
||||
double w = 11, h = 21;
|
||||
double x = 0, y = 0;
|
||||
double vx = 0, vy = 0;
|
||||
int jumps = 0;
|
||||
bool djUp = false;
|
||||
bool grounded = false;
|
||||
int facing = 1;
|
||||
bool alive = true;
|
||||
bool dead = false;
|
||||
int deadT = 0;
|
||||
double deathX = 0, deathY = 0;
|
||||
bool inWater = false;
|
||||
int animT = 0;
|
||||
int shootCool = 0;
|
||||
Mover? mover;
|
||||
bool sliding = false;
|
||||
bool justRespawned = false;
|
||||
bool hasGun = false;
|
||||
int apples = 0;
|
||||
SavePoint? savePoint;
|
||||
int saveIdx = 0;
|
||||
bool clearing = false;
|
||||
|
||||
// 回调(由 World 设置)
|
||||
void Function()? onDeath;
|
||||
void Function()? onClear;
|
||||
void Function(dynamic e)? onPortal;
|
||||
void Function()? onRespawnFace;
|
||||
|
||||
Player(this.level) {
|
||||
reset(true);
|
||||
}
|
||||
|
||||
double get cx => x + w / 2;
|
||||
double get cy => y + h / 2;
|
||||
|
||||
void reset(bool full) {
|
||||
final sp = (full || savePoint == null)
|
||||
? SavePoint(level.spawn.x, level.spawn.y, level.faceIndex ?? 0)
|
||||
: savePoint!;
|
||||
x = sp.x; y = sp.y;
|
||||
vx = 0; vy = 0;
|
||||
jumps = 0;
|
||||
djUp = false;
|
||||
grounded = false;
|
||||
facing = 1;
|
||||
alive = true;
|
||||
dead = false;
|
||||
deadT = 0;
|
||||
deathX = 0; deathY = 0;
|
||||
inWater = false;
|
||||
animT = 0;
|
||||
shootCool = 0;
|
||||
mover = null;
|
||||
sliding = false;
|
||||
justRespawned = false;
|
||||
if (full) {
|
||||
hasGun = level.data['startGun'] == true;
|
||||
apples = 0;
|
||||
savePoint = null;
|
||||
saveIdx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 纯死亡演出(不进死亡状态):粒子爆开 + 屏幕轻震 + 死亡点扩散冲击环。
|
||||
void deathBurst() {
|
||||
final L = level;
|
||||
final dx = clamp(cx, 10, max(10, L.pxW - 10));
|
||||
final dy = clamp(cy, 10, max(10, L.pxH - 10));
|
||||
Sfx.instance.death();
|
||||
L.particles.spawn(dx, dy, 16, colors: const ['#ff5c7a', '#ff8095', '#fff', '#ffc63a'], speed: 3.4, life: 34, size: 3, grav: 0.16);
|
||||
L.shake = max(L.shake, 6);
|
||||
L.addDeathFX(dx, dy);
|
||||
deathX = dx; deathY = dy;
|
||||
}
|
||||
|
||||
void kill([Map opts = const {}]) {
|
||||
if (dead || !alive) return;
|
||||
dead = true; alive = false; deadT = 0;
|
||||
vx = 0; vy = 0; sliding = false;
|
||||
deathBurst();
|
||||
if (opts['mark'] != false) level.addDeathMark(deathX, deathY);
|
||||
if (opts['count'] != false && onDeath != null) onDeath!();
|
||||
}
|
||||
|
||||
void respawn() {
|
||||
dead = false; alive = true;
|
||||
final L = level;
|
||||
final sp = savePoint ?? SavePoint(L.spawn.x, L.spawn.y, 0);
|
||||
x = sp.x; y = sp.y;
|
||||
vx = 0; vy = 0; jumps = 0;
|
||||
djUp = false;
|
||||
shootCool = 0; sliding = false;
|
||||
deadT = 0;
|
||||
int guard = 0;
|
||||
while (L.solidRect(x, y, w, h) && guard++ < 48) y -= 2;
|
||||
L.softReset();
|
||||
justRespawned = true;
|
||||
}
|
||||
|
||||
void update(int dt) {
|
||||
final L = level;
|
||||
animT++;
|
||||
if (shootCool > 0) shootCool--;
|
||||
|
||||
if (dead) { deadT++; return; }
|
||||
|
||||
final inWater = L.waterRect(x + 2, y + 4, w - 4, h - 4);
|
||||
if (inWater && !this.inWater) Sfx.instance.splash();
|
||||
this.inWater = inWater;
|
||||
|
||||
final moveL = Input.held('left'), moveR = Input.held('right');
|
||||
final onIce = grounded && L.iceAtBottom(x, y, w, h);
|
||||
double target = 0;
|
||||
if (moveL && !moveR) target = -PHY.HSPD;
|
||||
else if (moveR && !moveL) target = PHY.HSPD;
|
||||
|
||||
final ICE_MAX = PHY.HSPD * PHY.ICE_TOP;
|
||||
final decel = (double f) {
|
||||
if (vx.abs() <= f) vx = 0;
|
||||
else vx -= sgn(vx) * f;
|
||||
};
|
||||
|
||||
if (onIce) {
|
||||
if (target != 0) vx += sgn(target) * PHY.ICE_ACC;
|
||||
else decel(PHY.ICE_FRIC);
|
||||
vx = clamp(vx, -ICE_MAX, ICE_MAX);
|
||||
if (vx.abs() > PHY.SLIDE_MIN) sliding = true;
|
||||
} else if (sliding && vx.abs() > PHY.SLIDE_STOP) {
|
||||
if (target != 0) {
|
||||
final dir = sgn(vx);
|
||||
if (dir == 0 || sgn(target) == dir) {
|
||||
if (vx.abs() >= PHY.HSPD) {
|
||||
vx -= dir * PHY.SLIDE_EASE;
|
||||
if (vx.abs() < PHY.HSPD) { vx = target; sliding = false; }
|
||||
} else {
|
||||
vx = target; sliding = false;
|
||||
}
|
||||
} else {
|
||||
vx -= dir * PHY.SLIDE_BRAKE;
|
||||
if (sgn(vx) == sgn(target)) { vx = target; sliding = false; }
|
||||
}
|
||||
vx = clamp(vx, -ICE_MAX, ICE_MAX);
|
||||
} else {
|
||||
decel(grounded ? PHY.SLIDE_FRIC : PHY.SLIDE_AIR);
|
||||
if (vx == 0) sliding = false;
|
||||
}
|
||||
if (vx != 0 && vx.abs() < PHY.SLIDE_STOP) { vx = 0; sliding = false; }
|
||||
} else {
|
||||
vx = target;
|
||||
sliding = false;
|
||||
}
|
||||
if (target != 0) facing = sgn(target).toInt();
|
||||
|
||||
if (inWater) {
|
||||
if (Input.held('jump')) vy -= PHY.WATER_RISE;
|
||||
if (Input.held('down')) vy += 0.25;
|
||||
vy = clamp(vy, -3.2, PHY.WATER_FALL);
|
||||
jumps = 0; djUp = false;
|
||||
} else {
|
||||
if (Input.hit('jump')) {
|
||||
if (grounded || jumps == 0) {
|
||||
vy = -PHY.JUMP; jumps = 1; grounded = false;
|
||||
Sfx.instance.jump();
|
||||
L.particles.spawn(cx, y + h, 5, colors: const ['#fff', '#cfcfe0'], speed: 1.6, life: 16, size: 2, dir: -pi / 2, spread: 2);
|
||||
} else if (jumps == 1) {
|
||||
vy = max(vy, 0) * PHY.DJ_INERTIA - PHY.DJUMP; jumps = 2;
|
||||
djUp = vy < 0;
|
||||
Sfx.instance.djump();
|
||||
L.particles.spawn(cx, cy, 10, colors: const ['#fff', '#9ad8ff'], speed: 2.2, life: 20, size: 2, spread: pi * 2);
|
||||
}
|
||||
}
|
||||
if (!Input.held('jump') && vy < -3) vy = -3;
|
||||
vy = min(vy + (vy < 0 ? (djUp ? PHY.GRAV_DJ : PHY.GRAV) : PHY.GRAV_FALL), PHY.MAXFALL);
|
||||
if (djUp && vy >= 0) djUp = false;
|
||||
}
|
||||
|
||||
if (mover != null && !(mover!.gone)) {
|
||||
x += mover!.dx;
|
||||
y += mover!.dy;
|
||||
}
|
||||
|
||||
// X 轴移动
|
||||
final prevBottom = y + h;
|
||||
mover = null;
|
||||
double steps = vx.abs();
|
||||
final dx = sgn(vx).toDouble();
|
||||
while (steps > 0) {
|
||||
final m = min(1, steps);
|
||||
if (!L.solidRect(x + dx * m, y, w, h)) { x += dx * m; steps -= m; }
|
||||
else break;
|
||||
}
|
||||
if (L.solidRect(x + dx, y, w, h) && vx.abs() > 0) vx = 0;
|
||||
|
||||
// Y 轴移动
|
||||
final dropThrough = Input.held('down');
|
||||
steps = vy.abs();
|
||||
final dy = sgn(vy).toDouble();
|
||||
bool hitGround = false;
|
||||
while (steps > 0) {
|
||||
final m = min(1, steps);
|
||||
final ny = y + dy * m;
|
||||
final solidHere = L.solidRect(x, ny, w, h, prevBottom: prevBottom, dropThrough: dropThrough);
|
||||
if (!solidHere) { y = ny; steps -= m; }
|
||||
else {
|
||||
if (dy > 0) hitGround = true;
|
||||
vy = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (final m in L.movers) {
|
||||
if (m.gone) continue;
|
||||
if (x + w > m.x && x < m.x + m.w) {
|
||||
if (y + h >= m.y - 2 && y + h <= m.y + m.h) {
|
||||
mover = m; hitGround = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final wasAir = !grounded;
|
||||
grounded = hitGround || (vy == 0 && L.solidRect(x, y + 1, w, h));
|
||||
if (grounded) {
|
||||
jumps = 0; djUp = false;
|
||||
}
|
||||
|
||||
if (sliding && grounded && vx.abs() > PHY.HSPD && animT % 5 == 0) {
|
||||
L.particles.spawn(cx - sgn(vx) * 5, y + h - 2, 2, colors: const ['#dff4ff', '#8fd8f0', '#fff'], speed: 1.1, life: 16, size: 2, dir: -pi / 2, spread: 1.7, grav: 0.1);
|
||||
}
|
||||
|
||||
x = clamp(x, 0, L.pxW - w);
|
||||
if (y > L.pxH + 200) { kill(); return; }
|
||||
if (y < -300) { y = -300; vy = 0; }
|
||||
|
||||
if (L.deadlyRect(x, y, w, h)) { kill(); return; }
|
||||
|
||||
if (hasGun && Input.hit('shoot') && shootCool == 0 && L.bullets.length < 5) {
|
||||
shoot();
|
||||
}
|
||||
|
||||
touchEntities();
|
||||
|
||||
if (grounded) {
|
||||
final ty = ((y + h + 2) / TILE).floor();
|
||||
for (int tx = (x / TILE).floor(); tx <= ((x + w - 1) / TILE).floor(); tx++) {
|
||||
if (ty < L.tiles.length && tx >= 0 && tx < L.tiles[0].length && L.tiles[ty][tx] == Tile.vanish && L.vanishState(tx, ty) == 0) {
|
||||
L.setVanish(tx, ty, 1);
|
||||
L.vanishT ??= {};
|
||||
L.vanishT![ty * 1000 + tx] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void shoot() {
|
||||
final L = level;
|
||||
shootCool = 9;
|
||||
L.bullets.add(Bullet(
|
||||
x: facing > 0 ? x + w - 2 : x - 6,
|
||||
y: y + 8, w: 10.0, h: 5.0,
|
||||
vx: 9.0 * facing, vy: 0.0, life: 130,
|
||||
));
|
||||
Sfx.instance.shoot();
|
||||
L.particles.spawn(facing > 0 ? x + w : x, y + 10, 3, colors: const ['#fff6a0'], speed: 1.5, life: 10, size: 2);
|
||||
}
|
||||
|
||||
void touchEntities() {
|
||||
final L = level;
|
||||
for (final e in L.entities) {
|
||||
if (e.dead) continue;
|
||||
final hit = e.type == 'save'
|
||||
? overlap(Box(x - 6, y - 6, w + 12, h + 12), Box(e.x, e.y, e.w, e.h))
|
||||
: overlap(Box(x, y, w, h), Box(e.x, e.y, e.w, e.h));
|
||||
if (!hit) continue;
|
||||
switch (e.type) {
|
||||
case 'save':
|
||||
if (!e.active) {
|
||||
e.active = true;
|
||||
savePoint = SavePoint(e.x + e.w / 2 - w / 2, e.y + e.h - h, level.faceIndex ?? 0);
|
||||
saveIdx = e.no ?? 1;
|
||||
Sfx.instance.save();
|
||||
L.particles.spawn(e.x + e.w / 2, e.y + e.h / 2, 18, colors: const ['#b06bff', '#fff', '#e5d0ff'], speed: 2.6, life: 30);
|
||||
if (e.fake) { L.showToast('假的。', 90); kill(); return; }
|
||||
}
|
||||
break;
|
||||
case 'goal':
|
||||
if (!clearing) { clearing = true; if (onClear != null) onClear!(); }
|
||||
break;
|
||||
case 'gun':
|
||||
if (!e.taken) {
|
||||
e.taken = true; hasGun = true;
|
||||
L.particles.spawn(e.x + e.w / 2, e.y + e.h / 2, 12, colors: const ['#dfe0f0', '#fff'], speed: 2.2, life: 24);
|
||||
L.showToast('获得手枪!按 X 射击', 120);
|
||||
Sfx.instance.save();
|
||||
}
|
||||
break;
|
||||
case 'apple':
|
||||
if (!e.taken) { e.taken = true; apples++; Sfx.instance.save(); }
|
||||
break;
|
||||
case 'jumpref':
|
||||
if (animT % 4 == 0) jumps = 0;
|
||||
break;
|
||||
case 'warp':
|
||||
if (e.cool > 0) break;
|
||||
e.cool = 40;
|
||||
final to = e.to;
|
||||
if (to != null && to is Map && !to.containsKey('x') && !to.containsKey('y') && to['face'] is int) {
|
||||
if (onPortal != null) onPortal!(e);
|
||||
break;
|
||||
}
|
||||
double? nx, ny;
|
||||
if (to == 'spawn') {
|
||||
nx = L.spawn.x; ny = L.spawn.y;
|
||||
} else if (to != null && to is Map && to.containsKey('x')) {
|
||||
nx = (to['x'] as num) * TILE; ny = (to['y'] as num) * TILE;
|
||||
} else {
|
||||
final p = L.entities.firstWhere((o) => o.type == 'warp' && o.idx == e.pair, orElse: () => e);
|
||||
if (p != e) {
|
||||
nx = p.x + p.w / 2 - w / 2; ny = p.y + p.h - h;
|
||||
}
|
||||
}
|
||||
if (nx != null && ny != null) {
|
||||
x = nx; y = ny;
|
||||
vx = 0; vy = 0;
|
||||
final partner = L.entities.firstWhere((o) => o.type == 'warp' && o.idx == e.pair, orElse: () => e);
|
||||
if (partner != e) partner.cool = 40;
|
||||
Sfx.instance.warp();
|
||||
L.particles.spawn(cx, cy, 12, colors: const ['#a86bff', '#fff'], speed: 2.5, life: 24);
|
||||
}
|
||||
break;
|
||||
case 'sign':
|
||||
if (animT % 30 == 0) L.showToast(e.text ?? '', 90);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw(Canvas c) {
|
||||
if (dead) return;
|
||||
drawKid(c, this, animT);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user