ya2 · news · projects · code · about

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