1import { makeSprite, t, Device, DeviceSize } from "@replay/core";
2import { WebInputs } from "@replay/web";
3import { iOSInputs } from "@replay/swift";
4import { Bird, birdWidth, birdHeight } from "./bird";
5import { Pipe, PipeT, pipeWidth, pipeGap, getPipeYPositions } from "./pipe";
6
7const speedX = 2;
8const birdX = 0;
9
10type LevelProps = {
11 paused: boolean;
12 gameOver: () => void;
13};
14
15type LevelState = {
16 birdY: number;
17 birdGravity: number;
18 pipes: PipeT[];
19};
20
21export const Level = makeSprite<LevelProps, LevelState, WebInputs | iOSInputs>({
22 init({ device, props }) {
23 return {
24 birdY: 10,
25 birdGravity: -12,
26 pipes: props.paused ? [] : [newPipe(device)],
27 };
28 },
29
30 loop({ props, state, getInputs, device }) {
31 if (props.paused) {
32 return state;
33 }
34
35 const inputs = getInputs();
36
37 let { birdGravity, birdY, pipes } = state;
38
39 birdGravity += 0.8;
40 birdY -= birdGravity;
41
42 if (inputs.pointer.justPressed || inputs.keysJustPressed[" "]) {
43 birdGravity = -12;
44 }
45
46 const lastPipe = pipes[pipes.length - 1];
47 if (lastPipe.x < 140) {
48 pipes = [...pipes, newPipe(device)]
49
50 .filter(
51 (pipe) =>
52 pipe.x > -(device.size.width + device.size.widthMargin + pipeWidth)
53 );
54 }
55
56 if (didHitPipe(birdY, device.size, pipes)) {
57 props.gameOver();
58 }
59
60
61 pipes = pipes.map((pipe) => {
62 let passed = pipe.passed;
63 if (!passed && pipe.x < birdX - birdWidth / 2 - pipeWidth / 2) {
64
65 passed = true;
66 }
67 return { ...pipe, passed, x: pipe.x - speedX };
68 });
69
70 return {
71 birdGravity,
72 birdY,
73 pipes,
74 };
75 },
76
77 render({ state, device }) {
78 const { size } = device;
79 return [
80 t.rectangle({
81 color: "#add8e6",
82 width: size.width + size.widthMargin * 2,
83 height: size.height + size.heightMargin * 2,
84 }),
85 Bird({
86 id: "bird",
87 x: birdX,
88 y: state.birdY,
89 rotation: Math.max(-30, state.birdGravity * 3 - 30),
90 }),
91 ...state.pipes.map((pipe, index) =>
92 Pipe({
93 id: `pipe-${index}`,
94 pipe,
95 x: pipe.x,
96 })
97 ),
98 ];
99 },
100});
101
102function newPipe(device: Device): PipeT {
103 const height = device.size.height + device.size.heightMargin * 2;
104 const randomY = (height - pipeGap * 2) * (device.random() - 0.5);
105
106 return {
107 x: device.size.width + device.size.widthMargin + 50,
108 gapY: randomY,
109 passed: false,
110 };
111}
112
113function didHitPipe(birdY: number, size: DeviceSize, pipes: PipeT[]) {
114 if (
115 birdY - birdHeight / 2 < -(size.height / 2 + size.heightMargin) ||
116 birdY + birdHeight / 2 > size.height / 2 + size.heightMargin
117 ) {
118
119 return true;
120 }
121 for (const pipe of pipes) {
122 if (
123 pipe.x > birdX + birdWidth / 2 + pipeWidth / 2 ||
124 pipe.x < birdX - birdWidth / 2 - pipeWidth / 2
125 ) {
126
127 continue;
128 }
129 const {
130 yUpperTop,
131 yUpperBottom,
132 yLowerTop,
133 yLowerBottom,
134 } = getPipeYPositions(size, pipe.gapY);
135 const topPipeRect = {
136 x: pipe.x,
137 y: (yUpperTop + yUpperBottom) / 2,
138 width: pipeWidth,
139 height: yUpperTop - yUpperBottom,
140 };
141 const bottomPipeRect = {
142 x: pipe.x,
143 y: (yLowerTop + yLowerBottom) / 2,
144 width: pipeWidth,
145 height: yLowerTop - yLowerBottom,
146 };
147
148 const birdPoints = [
149 { x: birdX + birdWidth / 2, y: birdY + birdHeight / 2 },
150 { x: birdX + birdWidth / 2, y: birdY - birdHeight / 2 },
151 { x: birdX, y: birdY + birdHeight / 2 },
152 { x: birdX, y: birdY - birdHeight / 2 },
153 { x: birdX - birdWidth / 2, y: birdY + birdHeight / 2 },
154 { x: birdX - birdWidth / 2, y: birdY - birdHeight / 2 },
155 ];
156 if (
157 birdPoints.some(
158 (point) =>
159 pointInRect(point, topPipeRect) || pointInRect(point, bottomPipeRect)
160 )
161 ) {
162
163 return true;
164 }
165 }
166 return false;
167}
168
169function pointInRect(
170 point: { x: number; y: number },
171 rect: { x: number; y: number; height: number; width: number }
172) {
173 return (
174 point.x > rect.x - rect.width / 2 &&
175 point.x < rect.x + rect.width / 2 &&
176 point.y > rect.y - rect.height / 2 &&
177 point.y < rect.y + rect.height / 2
178 );
179}
180