1use ponko::*;
11
12const W: f32 = 1000.0;
13const H: f32 = 700.0;
14
15struct Bell {
17 rang: f32,
18}
19
20fn build_level(ctx: &mut Ctx) {
22 for (at, size) in [
24 (vec2(W / 2.0, H - 10.0), vec2(W, 20.0)),
25 (vec2(10.0, H / 2.0), vec2(20.0, H)),
26 (vec2(W - 10.0, H / 2.0), vec2(20.0, H)),
27 ] {
28 ctx.world
29 .spawn((Transform2D::at(at), RigidBody::fixed(), Collider::rect(size)));
30 }
31 ctx.world.spawn((
33 Transform2D::at(vec2(300.0, 100.0)),
34 RigidBody::dynamic(),
35 Collider::circle(16.0).restitution(0.8),
36 ));
37 ctx.world.spawn((
38 Transform2D::at(vec2(320.0, 0.0)),
39 RigidBody::dynamic(),
40 Collider::rect(vec2(40.0, 40.0)).friction(0.9),
41 ));
42 ctx.world.spawn((
44 Transform2D::at(vec2(150.0, H - 80.0)),
45 RigidBody::fixed(),
46 Collider::polygon(&[vec2(-140.0, 60.0), vec2(140.0, 60.0), vec2(-140.0, -60.0)]),
47 ));
48 ctx.world.spawn((
50 Transform2D::at(vec2(800.0, H - 60.0)),
51 RigidBody::fixed(),
52 Collider::rect(vec2(120.0, 80.0)).sensor(),
53 Bell { rang: 0.0 },
54 ));
55}
56fn ring_the_bell(ctx: &mut Ctx) {
60 let mut rang = false;
61 for c in ctx.events.read::<CollisionStarted>() {
62 rang |= c.sensor && [c.a, c.b].iter().any(|&e| ctx.world.has::<Bell>(e));
63 }
64 let dt = ctx.dt();
65 for bell in ctx.world.query::<&mut Bell>().iter() {
66 bell.rang = if rang { 1.0 } else { (bell.rang - dt * 2.0).max(0.0) };
67 }
68}
69fn blast(ctx: &mut Ctx) {
73 if !ctx.key_pressed(Key::Space) {
74 return;
75 }
76 let center = ctx.mouse_world();
77 let mut near = Vec::new();
78 ctx.physics.entities_in_circle(center, 200.0, &mut near);
79 for e in near {
80 let Some(t) = ctx.world.get::<Transform2D>(e) else {
81 continue;
82 };
83 let away = (t.position - center).normalize_or(Vec2::NEG_Y);
84 if let Some(mass) = ctx.physics.mass(e) {
85 ctx.physics.apply_impulse(e, away * mass * 600.0);
86 }
87 }
88}
89
90fn laser(ctx: &mut Ctx) -> Option<RayHit> {
92 ctx.physics.cast_ray(ctx.mouse_world(), vec2(0.0, 1.0), H)
93}
94fn drop_things(ctx: &mut Ctx) {
97 let at = ctx.mouse_world();
98 if ctx.mouse_pressed(MouseButton::Left) {
99 ctx.world.spawn((
100 Transform2D::at(at),
101 RigidBody::dynamic(),
102 Collider::rect(vec2(30.0, 30.0)).friction(0.8),
103 ));
104 }
105 if ctx.mouse_pressed(MouseButton::Right) {
106 ctx.world.spawn((
107 Transform2D::at(at),
108 RigidBody::dynamic(),
109 Collider::circle(12.0).restitution(0.6),
110 ));
111 }
112 if ctx.mouse_pressed(MouseButton::Middle) {
113 let rock: Vec<Vec2> = (0..5)
114 .map(|i| Vec2::from_angle(i as f32 * std::f32::consts::TAU / 5.0) * 18.0)
115 .collect();
116 ctx.world.spawn((
117 Transform2D::at(at),
118 RigidBody::dynamic(),
119 Collider::polygon(&rock).friction(0.7),
120 ));
121 }
122 if ctx.key_pressed(Key::R) {
123 let all: Vec<Entity> = ctx.world.query::<Entity>().with::<RigidBody>().iter().collect();
124 for e in all {
125 ctx.world.despawn(e);
126 }
127 build_level(ctx);
128 }
129 if ctx.key_pressed(Key::Escape) {
130 ctx.quit();
131 }
132}
133
134fn draw(ctx: &mut Ctx) {
135 ctx.clear(Color::hex(0x1d2330));
136 let shapes: Vec<(Transform2D, Collider, bool, f32)> = ctx
137 .world
138 .query::<(&Transform2D, &Collider, &RigidBody, Option<&Bell>)>()
139 .iter()
140 .map(|(t, c, b, bell)| (*t, *c, b.kind == BodyKind::Fixed, bell.map_or(0.0, |b| b.rang)))
141 .collect();
142 for (t, c, fixed, rang) in shapes {
143 let color = if c.sensor {
144 Color::rgba(1.0, 0.3 + 0.7 * (1.0 - rang), 0.3, 0.35)
145 } else if fixed {
146 Color::hex(0x4a5568)
147 } else {
148 Color::hex(0xf6ad55)
149 };
150 match c.shape {
151 Shape::Circle { radius } => {
152 ctx.draw_circle(t.position, radius, color);
153 let spoke = t.position + Vec2::from_angle(t.rotation) * radius;
155 ctx.draw_line(t.position, spoke, 2.0, Color::hex(0x1d2330));
156 }
157 Shape::Rect { size } => {
158 let (sin, cos) = t.rotation.sin_cos();
160 let turn = |v: Vec2| t.position + vec2(v.x * cos - v.y * sin, v.x * sin + v.y * cos);
161 let h = size / 2.0;
162 let corners = [vec2(-h.x, -h.y), vec2(h.x, -h.y), vec2(h.x, h.y), vec2(-h.x, h.y)].map(turn);
163 if fixed || c.sensor {
164 ctx.draw_rect(Rect::new(t.position.x - h.x, t.position.y - h.y, size.x, size.y), color);
165 } else {
166 for i in 0..4 {
167 ctx.draw_line(corners[i], corners[(i + 1) % 4], 3.0, color);
168 }
169 }
170 }
171 Shape::Capsule { height, radius } => {
172 let up = Vec2::from_angle(t.rotation + std::f32::consts::FRAC_PI_2) * height / 2.0;
173 ctx.draw_line(t.position - up, t.position + up, radius * 2.0, color);
174 }
175 Shape::Polygon { .. } => {
176 let (sin, cos) = t.rotation.sin_cos();
177 let turn = |v: Vec2| t.position + vec2(v.x * cos - v.y * sin, v.x * sin + v.y * cos);
178 let corners: Vec<Vec2> = c.shape.points().iter().map(|&p| turn(p + c.offset)).collect();
179 for i in 0..corners.len() {
180 ctx.draw_line(corners[i], corners[(i + 1) % corners.len()], 3.0, color);
181 }
182 }
183 _ => {} }
185 }
186 if let Some(hit) = laser(ctx) {
187 ctx.draw_line(ctx.mouse_world(), hit.point, 1.0, Color::rgba(1.0, 0.2, 0.2, 0.8));
188 ctx.draw_circle(hit.point, 4.0, Color::rgb(1.0, 0.2, 0.2));
189 }
190 let bodies = ctx.physics.body_count();
191 ctx.draw_text(
192 &format!(
193 "{bodies} bodies | left click: crate right click: ball middle click: rock space: blast R: reset"
194 ),
195 vec2(30.0, 30.0),
196 18.0,
197 Color::WHITE,
198 );
199}
200
201fn main() {
202 Game::new("Physics", W as u32, H as u32)
204 .physics(vec2(0.0, 980.0)) .sim_system(ring_the_bell)
206 .system(drop_things)
208 .system(blast)
209 .system(draw)
210 .run(build_level);
211}