Commit | Line | Data |
---|---|---|
c991401b | 1 | from panda3d.core import CullFaceAttrib, Point3, Texture, \ |
d5932612 | 2 | Plane, Vec3, BitMask32 |
c991401b | 3 | from panda3d.bullet import BulletRigidBodyNode, BulletGhostNode |
d5932612 | 4 | from direct.gui.OnscreenText import OnscreenText |
ee65fee0 | 5 | from direct.showbase.DirectObject import DirectObject |
bf77b5d5 | 6 | from ya2.utils.gfx import GfxTools, Point |
d5932612 FC |
7 | |
8 | ||
9 | class Command: | |
10 | ||
11 | def __init__(self, pos, rot): | |
12 | self.pos = pos | |
13 | self.rot = rot | |
14 | ||
15 | ||
2aeb9f68 FC |
16 | class ItemStrategy: pass |
17 | ||
18 | ||
19 | class FixedStrategy(ItemStrategy): | |
0e86689f | 20 | |
0a0994e4 | 21 | def win_condition(self): |
0e86689f FC |
22 | return True |
23 | ||
24 | ||
2aeb9f68 | 25 | class StillStrategy(ItemStrategy): |
0e86689f FC |
26 | |
27 | def __init__(self, np): | |
28 | self._np = np | |
29 | self._positions = [] | |
30 | self._rotations = [] | |
31 | ||
0a0994e4 | 32 | def win_condition(self): |
0e86689f FC |
33 | self._positions += [self._np.get_pos()] |
34 | self._rotations += [self._np.get_hpr()] | |
32cd89ca | 35 | if len(self._positions) > 30: |
0e86689f | 36 | self._positions.pop(0) |
32cd89ca | 37 | if len(self._rotations) > 30: |
0e86689f | 38 | self._rotations.pop(0) |
32cd89ca | 39 | if len(self._positions) < 28: |
0e86689f FC |
40 | return |
41 | avg_x = sum(pos.x for pos in self._positions) / len(self._positions) | |
42 | avg_y = sum(pos.y for pos in self._positions) / len(self._positions) | |
43 | avg_z = sum(pos.z for pos in self._positions) / len(self._positions) | |
44 | avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations) | |
45 | avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations) | |
46 | avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations) | |
47 | avg_pos = Point3(avg_x, avg_y, avg_z) | |
48 | avg_rot = Point3(avg_h, avg_p, avg_r) | |
49 | return all((pos - avg_pos).length() < .1 for pos in self._positions) and \ | |
50 | all((rot - avg_rot).length() < 1 for rot in self._rotations) | |
51 | ||
52 | ||
ee65fee0 | 53 | class Item(DirectObject): |
d5932612 | 54 | |
98741d67 | 55 | def __init__(self, world, plane_node, cb_inst, curr_bottom, scene_repos, model_path, json, model_scale=1, exp_num_contacts=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.5): |
ee65fee0 | 56 | super().__init__() |
d5932612 FC |
57 | self._world = world |
58 | self._plane_node = plane_node | |
59 | self._count = count | |
60 | self._cb_inst = cb_inst | |
61 | self._paused = False | |
62 | self._overlapping = False | |
63 | self._first_command = True | |
64 | self.repos_done = False | |
ef3c36bf FC |
65 | self._mass = mass |
66 | self._pos = pos | |
67 | self._r = r | |
98741d67 | 68 | self.json = json |
0e86689f | 69 | self.strategy = FixedStrategy() |
ea38777c | 70 | self._exp_num_contacts = exp_num_contacts |
d5932612 | 71 | self._curr_bottom = curr_bottom |
e1438449 | 72 | self._scene_repos = scene_repos |
8254721d | 73 | self._model_scale = model_scale |
d5932612 FC |
74 | self._model_path = model_path |
75 | self._commands = [] | |
76 | self._command_idx = -1 | |
e67dbe53 | 77 | self._restitution = restitution |
32cd89ca | 78 | self._friction = friction |
0a0994e4 FC |
79 | self._positions = [] |
80 | self._rotations = [] | |
ef3c36bf FC |
81 | if count: |
82 | self.node = BulletGhostNode(self.__class__.__name__) | |
83 | else: | |
84 | self.node = BulletRigidBodyNode(self.__class__.__name__) | |
80d579b1 | 85 | self._set_shape(count) |
bf77b5d5 | 86 | self._np = GfxTools.build_node_from_physics(self.node) |
ef3c36bf FC |
87 | if count: |
88 | world.attach_ghost(self.node) | |
89 | else: | |
90 | world.attach_rigid_body(self.node) | |
bf77b5d5 FC |
91 | self._model = GfxTools.build_model(model_path) |
92 | self._model.set_srgb_textures() | |
d5932612 FC |
93 | self._model.flatten_light() |
94 | self._model.reparent_to(self._np) | |
8254721d | 95 | self._np.set_scale(model_scale) |
80d579b1 | 96 | self._np.flatten_strong() |
ee65fee0 | 97 | self._set_outline_model() |
bf77b5d5 | 98 | self._outline_model.set_srgb_textures() |
ee65fee0 FC |
99 | self._model.hide(BitMask32(0x01)) |
100 | self._outline_model.hide(BitMask32(0x01)) | |
d5932612 FC |
101 | self._start_drag_pos = None |
102 | self._prev_rot_info = None | |
103 | self._last_nonoverlapping_pos = None | |
104 | self._last_nonoverlapping_rot = None | |
ef3c36bf | 105 | self._instantiated = not count |
d5932612 | 106 | self._box_tsk = taskMgr.add(self.on_frame, 'on_frame') |
ef3c36bf FC |
107 | if count: |
108 | self._repos() | |
109 | else: | |
110 | self._np.set_pos(pos) | |
111 | self._np.set_r(r) | |
ee65fee0 | 112 | self.__editing = False |
bf77b5d5 | 113 | self.__interactable = count |
ee65fee0 FC |
114 | self.accept('editor-start', self.__editor_start) |
115 | self.accept('editor-stop', self.__editor_stop) | |
d5932612 | 116 | |
80d579b1 | 117 | def _set_shape(self, apply_scale=True): |
ea38777c | 118 | pass |
d5932612 | 119 | |
ee65fee0 FC |
120 | def __editor_start(self): |
121 | self.__editing = True | |
122 | ||
123 | def __editor_stop(self): | |
124 | self.__editing = False | |
125 | ||
126 | @property | |
127 | def interactable(self): | |
bf77b5d5 | 128 | return self.__editing or self.__interactable |
ee65fee0 | 129 | |
0e86689f FC |
130 | def set_strategy(self, strategy): |
131 | self.strategy = strategy | |
132 | ||
d5932612 | 133 | def _repos(self): |
bf77b5d5 | 134 | p_from, p_to = Point((-1, 1)).from_to_points() |
d5932612 FC |
135 | for hit in self._world.ray_test_all(p_from, p_to).get_hits(): |
136 | if hit.get_node() == self._plane_node: | |
137 | pos = hit.get_hit_pos() | |
c991401b | 138 | #corner = P3dGfxMgr.screen_coord(pos) |
d5932612 FC |
139 | bounds = self._np.get_tight_bounds() |
140 | bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos() | |
141 | self._np.set_pos(pos) | |
142 | plane = Plane(Vec3(0, 1, 0), bounds[0][1]) | |
143 | pos3d, near_pt, far_pt = Point3(), Point3(), Point3() | |
144 | margin, ar = .04, base.get_aspect_ratio() | |
145 | margin_x = margin / ar if ar >= 1 else margin | |
146 | margin_z = margin * ar if ar < 1 else margin | |
147 | top = self._curr_bottom() | |
148 | base.camLens.extrude((-1 + margin_x, top - margin_z), near_pt, far_pt) | |
149 | plane.intersects_line( | |
150 | pos3d, render.get_relative_point(base.camera, near_pt), | |
151 | render.get_relative_point(base.camera, far_pt)) | |
152 | delta = Vec3(bounds[1][0], bounds[1][1], bounds[0][2]) | |
153 | self._np.set_pos(pos3d + delta) | |
154 | if not hasattr(self, '_txt'): | |
155 | font = base.loader.load_font('assets/fonts/Hanken-Book.ttf') | |
156 | font.clear() | |
157 | font.set_pixels_per_unit(60) | |
158 | font.set_minfilter(Texture.FTLinearMipmapLinear) | |
159 | font.set_outline((0, 0, 0, 1), .8, .2) | |
160 | self._txt = OnscreenText( | |
161 | str(self._count), font=font, scale=0.06, fg=(.9, .9, .9, 1)) | |
162 | pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2]) | |
bf77b5d5 | 163 | p2d = Point(pos).screen_coord() |
d5932612 FC |
164 | self._txt['pos'] = p2d |
165 | self.repos_done = True | |
166 | ||
e1438449 FC |
167 | def repos_x(self, x): |
168 | self._np.set_x(x) | |
169 | bounds = self._np.get_tight_bounds() | |
170 | bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos() | |
171 | pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2]) | |
bf77b5d5 | 172 | p2d = Point(pos).screen_coord() |
e1438449 FC |
173 | self._txt['pos'] = p2d |
174 | ||
d5932612 FC |
175 | def get_bottom(self): |
176 | bounds = self._np.get_tight_bounds() | |
177 | bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos() | |
178 | pos = self._np.get_pos() + (bounds[1][0], bounds[1][1], bounds[0][2]) | |
bf77b5d5 | 179 | p2d = pos.screen_coord() |
d5932612 FC |
180 | ar = base.get_aspect_ratio() |
181 | return p2d[1] if ar >= 1 else (p2d[1] * ar) | |
182 | ||
183 | def get_corner(self): | |
184 | bounds = self._np.get_tight_bounds() | |
185 | return bounds[1][0], bounds[1][1], bounds[0][2] | |
186 | ||
187 | def _set_outline_model(self): | |
bf77b5d5 | 188 | self._outline_model = GfxTools.build_model(self._model_path) |
ea38777c FC |
189 | #clockw = CullFaceAttrib.MCullClockwise |
190 | #self._outline_model.set_attrib(CullFaceAttrib.make(clockw)) | |
191 | self._outline_model.set_attrib(CullFaceAttrib.make_reverse()) | |
d5932612 | 192 | self._outline_model.reparent_to(self._np) |
ea38777c | 193 | self._outline_model.set_scale(1.08) |
d5932612 FC |
194 | self._outline_model.set_light_off() |
195 | self._outline_model.set_color(.4, .4, .4, 1) | |
196 | self._outline_model.set_color_scale(.4, .4, .4, 1) | |
197 | self._outline_model.hide() | |
198 | ||
199 | def on_frame(self, task): | |
200 | self._np.set_y(0) | |
201 | return task.cont | |
202 | ||
203 | def undo(self): | |
204 | self._command_idx -= 1 | |
205 | self._np.set_pos(self._commands[self._command_idx].pos) | |
206 | self._np.set_hpr(self._commands[self._command_idx].rot) | |
207 | ||
208 | def redo(self): | |
209 | self._command_idx += 1 | |
210 | self._np.set_pos(self._commands[self._command_idx].pos) | |
211 | self._np.set_hpr(self._commands[self._command_idx].rot) | |
212 | ||
213 | def play(self): | |
214 | if not self._instantiated: | |
215 | return | |
216 | self._world.remove_rigid_body(self.node) | |
ef3c36bf | 217 | self.node.set_mass(self._mass) |
d5932612 | 218 | self._world.attach_rigid_body(self.node) |
e67dbe53 | 219 | self.node.set_restitution(self._restitution) |
32cd89ca | 220 | self.node.set_friction(self._friction) |
d5932612 FC |
221 | |
222 | def on_click_l(self, pos): | |
223 | if self._paused: return | |
ee65fee0 | 224 | if self.__editing: |
2aeb9f68 | 225 | messenger.send('editor-item-click', [self]) |
d5932612 FC |
226 | self._start_drag_pos = pos, self._np.get_pos() |
227 | loader.load_sfx('assets/audio/sfx/grab.ogg').play() | |
228 | if not self._instantiated: | |
229 | self._world.remove_ghost(self.node) | |
230 | pos = self._np.get_pos() | |
231 | self._np.remove_node() | |
232 | self.node = BulletRigidBodyNode('box') | |
ea38777c | 233 | self._set_shape() |
d5932612 FC |
234 | self._np = render.attach_new_node(self.node) |
235 | self._world.attach_rigid_body(self.node) | |
236 | self._model.reparent_to(self._np) | |
237 | self._np.set_pos(pos) | |
238 | self._set_outline_model() | |
8254721d | 239 | self._np.set_scale(self._model_scale) |
d5932612 FC |
240 | self._model.show(BitMask32(0x01)) |
241 | self._outline_model.hide(BitMask32(0x01)) | |
242 | self._instantiated = True | |
243 | self._txt.destroy() | |
244 | self._count -= 1 | |
245 | if self._count: | |
98741d67 | 246 | item = self.__class__(self._world, self._plane_node, self._cb_inst, self._curr_bottom, self._scene_repos, None, count=self._count, mass=self._mass, pos=self._pos, r=self._r) # pylint: disable=no-value-for-parameter |
d5932612 | 247 | self._cb_inst(item) |
e1438449 | 248 | self._scene_repos() |
d5932612 FC |
249 | |
250 | def on_click_r(self, pos): | |
a6843832 | 251 | if self._paused or not self._instantiated: return |
2aeb9f68 FC |
252 | if self.__editing: |
253 | messenger.send('editor-item-click', [self]) | |
d5932612 FC |
254 | self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r() |
255 | loader.load_sfx('assets/audio/sfx/grab.ogg').play() | |
256 | ||
257 | def on_release(self): | |
258 | if self._start_drag_pos or self._prev_rot_info: | |
259 | loader.load_sfx('assets/audio/sfx/release.ogg').play() | |
260 | self._command_idx += 1 | |
261 | self._commands = self._commands[:self._command_idx] | |
262 | self._commands += [Command(self._np.get_pos(), self._np.get_hpr())] | |
263 | self._first_command = False | |
264 | self._start_drag_pos = self._prev_rot_info = None | |
265 | if self._overlapping: | |
266 | self._np.set_pos(self._last_nonoverlapping_pos) | |
267 | self._np.set_hpr(self._last_nonoverlapping_rot) | |
a6843832 FC |
268 | self._outline_model.set_color(.4, .4, .4, 1) |
269 | self._outline_model.set_color_scale(.4, .4, .4, 1) | |
93cf86b2 | 270 | self._overlapping = False |
d5932612 FC |
271 | |
272 | def on_mouse_on(self): | |
ef3c36bf | 273 | if not self._paused and self.interactable: |
d5932612 FC |
274 | self._outline_model.show() |
275 | ||
276 | def on_mouse_off(self): | |
277 | if self._start_drag_pos or self._prev_rot_info: return | |
ef3c36bf FC |
278 | if self.interactable: |
279 | self._outline_model.hide() | |
d5932612 FC |
280 | |
281 | def on_mouse_move(self, pos): | |
282 | if self._start_drag_pos: | |
283 | d_pos = pos - self._start_drag_pos[0] | |
284 | self._np.set_pos(self._start_drag_pos[1] + d_pos) | |
285 | if self._prev_rot_info: | |
286 | start_vec = self._prev_rot_info[0] - self._prev_rot_info[1] | |
287 | curr_vec = pos - self._prev_rot_info[1] | |
288 | d_angle = curr_vec.signed_angle_deg(start_vec, (0, -1, 0)) | |
289 | self._np.set_r(self._prev_rot_info[2] + d_angle) | |
290 | self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r() | |
291 | if self._start_drag_pos or self._prev_rot_info: | |
292 | res = self._world.contact_test(self.node) | |
293 | nres = res.get_num_contacts() | |
ea38777c | 294 | if nres <= self._exp_num_contacts: |
d5932612 FC |
295 | self._overlapping = False |
296 | self._outline_model.set_color(.4, .4, .4, 1) | |
297 | self._outline_model.set_color_scale(.4, .4, .4, 1) | |
ea38777c | 298 | if nres > self._exp_num_contacts and not self._overlapping: |
d5932612 FC |
299 | actual_nres = 0 |
300 | for contact in res.get_contacts(): | |
301 | for node in [contact.get_node0(), contact.get_node1()]: | |
302 | if isinstance(node, BulletRigidBodyNode) and \ | |
303 | node != self.node: | |
304 | actual_nres += 1 | |
305 | if actual_nres >= 1: | |
306 | self._overlapping = True | |
307 | loader.load_sfx('assets/audio/sfx/overlap.ogg').play() | |
308 | self._outline_model.set_color(.9, .1, .1, 1) | |
309 | self._outline_model.set_color_scale(.9, .1, .1, 1) | |
310 | if not self._overlapping: | |
311 | self._last_nonoverlapping_pos = self._np.get_pos() | |
312 | self._last_nonoverlapping_rot = self._np.get_hpr() | |
ee65fee0 | 313 | messenger.send('item-rototranslated', [self._np]) |
d5932612 FC |
314 | |
315 | def on_aspect_ratio_changed(self): | |
316 | if not self._instantiated: | |
317 | self._repos() | |
318 | ||
319 | def store_state(self): | |
320 | self._paused = True | |
321 | self._model.set_transparency(True) | |
322 | self._model.set_alpha_scale(.3) | |
ef3c36bf | 323 | if hasattr(self, '_txt') and not self._txt.is_empty(): |
d5932612 FC |
324 | self._txt.set_alpha_scale(.3) |
325 | ||
326 | def restore_state(self): | |
327 | self._paused = False | |
328 | self._model.set_alpha_scale(1) | |
ef3c36bf | 329 | if hasattr(self, '_txt') and not self._txt.is_empty(): |
d5932612 FC |
330 | self._txt.set_alpha_scale(1) |
331 | ||
0a0994e4 | 332 | def fail_condition(self): |
32cd89ca | 333 | if self._np.get_z() < -6: |
0a0994e4 FC |
334 | return True |
335 | self._positions += [self._np.get_pos()] | |
336 | self._rotations += [self._np.get_hpr()] | |
32cd89ca | 337 | if len(self._positions) > 30: |
0a0994e4 | 338 | self._positions.pop(0) |
32cd89ca | 339 | if len(self._rotations) > 30: |
0a0994e4 | 340 | self._rotations.pop(0) |
32cd89ca | 341 | if len(self._positions) < 28: |
0a0994e4 FC |
342 | return |
343 | avg_x = sum(pos.x for pos in self._positions) / len(self._positions) | |
344 | avg_y = sum(pos.y for pos in self._positions) / len(self._positions) | |
345 | avg_z = sum(pos.z for pos in self._positions) / len(self._positions) | |
346 | avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations) | |
347 | avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations) | |
348 | avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations) | |
349 | avg_pos = Point3(avg_x, avg_y, avg_z) | |
350 | avg_rot = Point3(avg_h, avg_p, avg_r) | |
351 | return all((pos - avg_pos).length() < .1 for pos in self._positions) and \ | |
352 | all((rot - avg_rot).length() < 1 for rot in self._rotations) | |
353 | ||
ee65fee0 FC |
354 | @property |
355 | def mass(self): | |
356 | return self._mass | |
357 | ||
358 | @mass.setter | |
359 | def mass(self, val): | |
360 | self._mass = val | |
361 | self.json['mass'] = val | |
362 | ||
363 | @property | |
364 | def restitution(self): | |
365 | return self._restitution | |
366 | ||
367 | @restitution.setter | |
368 | def restitution(self, val): | |
369 | self._restitution = val | |
370 | self.json['restitution'] = val | |
371 | ||
372 | @property | |
373 | def friction(self): | |
374 | return self._friction | |
375 | ||
376 | @friction.setter | |
377 | def friction(self, val): | |
378 | self._friction = val | |
379 | self.json['friction'] = val | |
380 | ||
381 | @property | |
382 | def position(self): | |
2aeb9f68 | 383 | return self._np.get_pos() |
ee65fee0 FC |
384 | |
385 | @position.setter | |
386 | def position(self, val): | |
387 | self._np.set_pos(*val) | |
388 | self.json['position'] = val | |
389 | ||
390 | @property | |
391 | def roll(self): | |
392 | return self._np.get_r() | |
393 | ||
394 | @roll.setter | |
395 | def roll(self, val): | |
396 | self._np.set_r(val) | |
397 | self.json['roll'] = val | |
398 | ||
399 | @property | |
400 | def scale(self): | |
401 | return self._np.get_scale()[0] | |
402 | ||
403 | @scale.setter | |
404 | def scale(self, val): | |
405 | self._np.set_scale(val) | |
406 | self.json['model_scale'] = val | |
407 | ||
2aeb9f68 FC |
408 | @property |
409 | def id(self): | |
410 | if 'id' in self.json: | |
411 | return self.json['id'] | |
412 | return None | |
413 | ||
414 | @id.setter | |
415 | def id(self, val): | |
416 | self.json['id'] = val | |
417 | ||
418 | @property | |
419 | def strategy_json(self): | |
420 | return self.json['strategy'] | |
421 | ||
422 | @strategy_json.setter | |
423 | def strategy_json(self, val): | |
424 | self.json['strategy'] = val | |
425 | ||
426 | @property | |
427 | def strategy_args_json(self): | |
428 | return self.json['strategy_args'] | |
429 | ||
430 | @strategy_args_json.setter | |
431 | def strategy_args_json(self, val): | |
432 | self.json['strategy_args'] = val.split(' ') | |
433 | ||
d5932612 FC |
434 | def destroy(self): |
435 | self._np.remove_node() | |
436 | taskMgr.remove(self._box_tsk) | |
ef3c36bf FC |
437 | if hasattr(self, '_txt'): |
438 | self._txt.destroy() | |
d5932612 FC |
439 | if not self._instantiated: |
440 | self._world.remove_ghost(self.node) | |
441 | else: | |
442 | self._world.remove_rigid_body(self.node) | |
3e3c4caf FC |
443 | self.ignore('editor-start') |
444 | self.ignore('editor-stop') |